Approccio più rapido all'ordinamento dei dati


11

Devo ordinare un bedfile in modo casuale 10000 volte e prendere le prime 1000 righe ogni volta. Attualmente sto usando il seguente codice:

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

Ci vogliono quasi 6 ore per fare questo per ogni file. Ne ho circa 150 da elaborare. C'è una soluzione più veloce per questo?

Un esempio dei dati (myfile.bed_sorted) che ho:

    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

1
Quanto è grande il tuo file e quanto è severa la tua nozione di "casuale"? splitpuò, err, dividere un file in pezzi di 1000 linee ciascuno, in modo da ottenere più file in una singola chiamata di sort. Inoltre, hai verificato se headè leggermente più veloce di tailperché non è necessario leggere l'intero file?
Ulrich Schwarz,

@UlrichSchwarz: il file di esempio che ho incollato sopra contiene circa 33000 righe. In generale, tutti i file del mio letto avranno più o meno lo stesso numero di righe. Anche per esempio: da un file di 33000 righe, non desidero ottenere 33 sottoinsiemi (1000 righe in ciascuno) in una singola esecuzione. Vorrei solo prendere le prime 1000 file di ogni corsa. Farò anche una coda dello stesso file. Solo per esempio, ho usato headqui.
biobudhan,

Secondo la pagina man sort -Rusa un "hash casuale di chiavi". La creazione dell'hash è una totale perdita di tempo e probabilmente richiede più tempo di ogni altra cosa. Sarebbe meglio leggere le righe in un array e poi mescolarle usando gli indici. Personalmente, lo userei perlper quello; potresti farlo con bashma avrai bisogno di una funzione per generare numeri casuali.
Riccioli d'oro,

@goldilocks: non sono una perlpersona! Mi potrebbe aiutare?
biobudhan,

6
Prova shufinvece sort -R, è considerevolmente più veloce. Naturalmente, farlo in memoria (vedi risposta Perl) batterà tutto ciò che richiede la rilettura dell'intero file nella shell.
frostschutz,

Risposte:


14

Supponendo che tu abbia memoria sufficiente per slurp il file, potresti provare

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

Dato che vuoi farlo 10000 volte, consiglierei di integrare la ripetizione nello script e di mescolare gli indici invece dell'array stesso per accelerare le cose:

$ 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

Quanto sopra ha creato 10000 file di 1000 righe ciascuno da un file che conteneva 37000 righe (il file di esempio è stato ripetuto 1000 volte). Come puoi vedere, ci sono voluti poco più di tre minuti sul mio sistema.

Spiegazione

  • use List::Util 'shuffle';: questo importa un modulo Perl che fornisce la shuffle()funzione che randomizza un array.
  • @l=<>;: carica il file di input ( <>) nell'array @l.
  • for $i (1..10000){} : eseguilo 10000 volte.
  • @r=shuffle(0..$#l);: $#lè il numero di elementi, @lquindi @rora è un elenco casuale dei numeri di indice dell'array @l(le righe del file di input).
  • open(my $fh, ">","file.$i.bed");: apre un file chiamato file.$i.bedper la scrittura. $iprenderà valori da 1 a 10000.
  • print $fh @l[@r[0..999]]: prende i primi 1000 indici nell'array mischiato e stampa le linee corrispondenti (elementi di @l).

Un altro approccio è usare shuf( grazie a @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

Wow!! Questo è impressionante!! Ha funzionato in 2 minuti :-) Ho solo un'altra domanda. Che ne dici di recuperare anche le ultime 1000 righe del file? Perché abbiamo bisogno di conoscere la lunghezza (numero di righe) nel file per ottenere questo? Per favore aiuto!
biobudhan,

1
@biobudhan considerano che shuf, come suggerito dal frostschutz: for i in {1..10000}; do shuf -n 1000 file.bed > file.$i.bed; done. Ci sono voluti ~ 1 minuto sul mio sistema. Per quanto riguarda le ultime 1000 linee, tutto ciò che serve è tail -n 1000.
terdon

1
@biobudhan vede anche la risposta aggiornata per una versione perl 3 volte più veloce.
terdon

Sì, l'ho provato e ora funziona più velocemente !! Grazie mille!!! :-)
biobudhan,

Hai ricontrollato i file di output della versione perl? Mi sembra strano che abbia così poco systempo, che sarebbe I / O dei file - questo non dovrebbe essere così totalmente diverso da shufquello, che ha ~ 30s sys. Quindi ho provato quello perl qui (cut n 'paste) e O_O ha creato 1000 file ma tutti i file erano vuoti ...
goldilocks

9

Se vuoi un benchmark per vedere quanto velocemente può essere fatto, copialo incolla 10kshuffle.cppe compila g++ 10kshuffle.cpp -o 10kshuffle. È quindi possibile eseguirlo:

10kshuffle filename < inputfile

Dove si filenametrova un percorso di base da utilizzare per i file di output; saranno nominati filename.0, filename.1ecc. e ognuno contiene le prime 1000 righe di uno shuffle. Scrive il nome di ciascun file mentre procede.

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

Su un singolo core da 3,5 Ghz, questo funziona in ~ 20 secondi:

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

data.txtera 37000 righe duplicate dalla domanda. Se si desidera l'intero shuffle nel file di output anziché le prime 1000 righe, modificare la riga 54 in:

for (int j = 0; j < copy.size(); j++) file << copy[j] << endl; 

3

Quindi c'è un aspetto Unix nella tua domanda, ma vale la pena risolvere prima il tuo problema fondamentale e poi provare a trovare un modo Unix-y per implementare quella soluzione.

È necessario creare 10.000 campioni di dimensioni 1.000 ciascuno da un file con un numero elevato e sconosciuto di righe. È possibile farlo in un unico passaggio del file se è possibile contenere 10.000 x 1.000 righe in memoria. Se non riesci a contenere tante righe in memoria, puoi comunque farlo in un unico passaggio se sai quante righe contiene il tuo file. Se non sai quante righe contiene il tuo file, devi contare un passaggio aggiuntivo per contare il numero di righe.

L'algoritmo, nel caso più difficile quando non si conosce il numero di righe, consiste nel fare quanto segue per ciascun campione (in parallelo, mantenendo i campioni in memoria):

  • includere le prime 1.000 righe nell'esempio
  • per l'ennesima riga (dove n > 1000), includila con la probabilità 1000 / ned elimina una riga casuale dalle righe che hai già selezionato. (a causa della probabilità di scartare alcune righe, dobbiamo conservare il campione in memoria fino alla fine dell'input)

Un modo elegante per implementare il secondo passo è generare un numero intero casuale kin [1, n]. Se k <= 1000quindi includere la riga e sostituire la kriga esistente esistente con essa. Ecco una descrizione più standard dell'algoritmo: http://en.wikipedia.org/wiki/Reservoir_sampling

Se conosci il numero di righe R, quindi:

  • inizia con la dimensione del campione, sdi 0
  • includere l'ennesima riga con probabilità (1000 - s) / (R - n + 1)e produrla immediatamente (e aumentare la dimensione del campione s)

Come fare questo su Unix? awksembra essere la risposta per questo post su Internet (non posso garantire la sua correttezza, ma il codice è lì) https://news.ycombinator.com/item?id=4840043

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.