Enfoque de clasificación de datos más rápido

11

Necesito ordenar un bedarchivo al azar 10000 veces y tomar las 1000 filas superiores cada vez. Actualmente, estoy usando el siguiente código:

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

Se tarda casi 6 horas en hacer esto para cada archivo. Tengo alrededor de 150 de ellos para resolver. ¿Hay una solución más rápida para esto?

Una muestra de los datos (myfile.bed_sorted) que tengo:

    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
fuente
1
¿Qué tan grande es su archivo y qué tan estricta es su noción de "aleatorio"? splitpuede, err, dividir un archivo en pedazos de 1000 líneas cada uno, para obtener más archivos en una sola llamada sort. Además, ¿ha verificado si heades un poco más rápido que tailporque no necesita leer todo el archivo?
Ulrich Schwarz
@UlrichSchwarz: el archivo de muestra que he pegado anteriormente contiene alrededor de 33000 filas. En general, todos mis archivos de cama tendrán más o menos el mismo número de filas. También, por ejemplo: desde un archivo de 33000 filas, no deseo obtener 33 subconjuntos (1000 filas en cada uno) en una sola ejecución. Solo deseo tomar las 1000 filas superiores de cada ejecución. También haré una cola del mismo archivo. Solo como muestra, lo usé headaquí.
biobudhan
Según la página del manual, se sort -Rutiliza un "hash aleatorio de claves". Crear el hash es una pérdida total de tiempo y probablemente lleva más tiempo que cualquier otra cosa. Sería mejor leer las líneas en una matriz y luego mezclarlas usando índices. Personalmente, lo usaría perlpara eso; podrías hacerlo bashpero necesitarás una función para generar números aleatorios.
Ricitos
@goldilocks: ¡No soy una perlpersona! ¿Podrías ayudarme por favor?
biobudhan
66
Intente en shuflugar de hacerlo sort -R, es considerablemente más rápido. Por supuesto, hacerlo en la memoria (ver la respuesta de Perl) superará cualquier cosa que requiera volver a leer todo el archivo en el shell.
frostschutz

Respuestas:

14

Suponiendo que tiene suficiente memoria para sorber el archivo, podría intentar

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

Como desea hacer esto 10000 veces, recomendaría integrar la repetición en el guión y mezclar los índices en lugar de la matriz en sí para acelerar las cosas:

$ 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

Lo anterior creó 10000 archivos de 1000 líneas cada uno de un archivo que contenía 37000 filas (su archivo de ejemplo se repitió 1000 veces). Como puede ver, tardó un poco más de tres minutos en mi sistema.

Explicación

  • use List::Util 'shuffle';: esto importa un módulo Perl que proporciona la shuffle()función que aleatoriza una matriz.
  • @l=<>;: carga el archivo de entrada ( <>) en la matriz @l.
  • for $i (1..10000){} : ejecuta esto 10000 veces.
  • @r=shuffle(0..$#l);: $#les el número de elementos, por @llo @rque ahora es una lista aleatoria de los números de índice de la matriz @l(las líneas del archivo de entrada).
  • open(my $fh, ">","file.$i.bed");: abre un archivo llamado file.$i.bedpara escritura. $itomará valores de 1 a 10000.
  • print $fh @l[@r[0..999]]: tome los primeros 1000 índices en la matriz aleatoria e imprima las líneas correspondientes (elementos de @l).

Otro enfoque es usar shuf( gracias @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
fuente
¡¡Guau!! ¡Eso es asombroso! Funcionó en 2 minutos :-) Solo tengo una pregunta más. ¿Qué hay de recuperar también las últimas 1000 líneas del archivo? ¿Porque necesitamos saber la longitud (número de líneas) en el archivo para lograr esto? ¡Por favor ayuda!
biobudhan
1
@biobudhan ¿Se considera shufcomo sugiere frostschutz: for i in {1..10000}; do shuf -n 1000 file.bed > file.$i.bed; done. Eso tomó ~ 1 minuto en mi sistema. En cuanto a las últimas 1000 líneas, todo lo que necesitas es tail -n 1000.
terdon
1
@biobudhan también ve una respuesta actualizada para una versión perl 3 veces más rápida.
terdon
¡Sí, lo probé y ahora funciona más rápido! ¡¡¡Muchas gracias!!! :-)
biobudhan
¿Verificó dos veces los archivos de salida de la versión perl? Me parece extraño que tenga tan poco systiempo, que sería E / S de archivo, esto no debería ser tan diferente al shufque tiene ~ 30 segundos sys. Así que probé el perl aquí (cortar y pegar) y O_O creó 1000 archivos pero todos los archivos estaban vacíos ...
goldilocks
9

Si desea un punto de referencia para ver qué tan rápido se puede hacer, copie y pegue esto 10kshuffle.cppy compílelo g++ 10kshuffle.cpp -o 10kshuffle. Luego puedes ejecutarlo:

10kshuffle filename < inputfile

Dónde filenamees una ruta base para usar para los archivos de salida; serán nombrados filename.0, filename.1etc., y cada uno contiene las primeras 1000 líneas de una mezcla aleatoria. Escribe el nombre de cada archivo a medida que avanza.

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

En un solo núcleo de 3.5 Ghz, esto se ejecuta en ~ 20 segundos:

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

data.txtse duplicaron 37000 líneas de la pregunta. Si desea la barajadura completa en el archivo de salida en lugar de las primeras 1000 líneas, cambie la línea 54 a:

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

Entonces, hay un aspecto de Unix en su pregunta, pero vale la pena resolver su problema fundamental primero y luego tratar de encontrar una forma de Unix para implementar esa solución.

Debe crear 10,000 muestras de tamaño 1,000 cada una a partir de un archivo con un gran número desconocido de filas. Es posible hacer esto en una sola pasada del archivo si puede contener 10,000 x 1,000 filas en la memoria. Si no puede mantener tantas filas en la memoria, puede hacerlo de una sola pasada si sabe cuántas filas contiene su archivo. Si no sabe cuántas filas contiene su archivo, necesita una pasada adicional para contar el número de filas.

El algoritmo, en el caso más difícil cuando no conoce el número de filas, es hacer lo siguiente para cada muestra (en paralelo, manteniendo las muestras en la memoria):

  • incluir las primeras 1,000 filas en la muestra
  • para la enésima fila (donde n > 1000), inclúyala con la probabilidad 1000 / ny descarte una fila aleatoria de las filas que ya ha seleccionado. (debido a la probabilidad de descartar algunas filas, necesitamos mantener la muestra en la memoria hasta el final de la entrada)

Una manera elegante de aplicar el segundo paso es generar un entero aleatorio ken [1, n]. Si k <= 1000luego incluye la fila y reemplaza la kfila-ésima existente con ella. Aquí hay una descripción más estándar del algoritmo: http://en.wikipedia.org/wiki/Reservoir_sampling

Si conoce el número de filas R, entonces:

  • comenzar con tamaño de muestra, sde 0
  • incluya la enésima fila con probabilidad (1000 - s) / (R - n + 1)y envíela de inmediato (e incremente el tamaño de la muestra s)

¿Cómo hacer esto en Unix? awkparece ser la respuesta según esta publicación en Internet (no puedo garantizar su corrección, pero el código está ahí) https://news.ycombinator.com/item?id=4840043

nigromante
fuente