Сортировка данных быстрее, подход

11

Мне нужно отсортировать bedфайл случайным образом 10000 раз и каждый раз брать первые 1000 строк. В настоящее время я использую следующий код:

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

Это занимает почти 6 часов, чтобы сделать это для каждого файла. У меня есть около 150 из них для разработки. Есть ли более быстрое решение для этого?

Образец данных (myfile.bed_sorted) у меня есть:

    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
источник
1
Насколько велик ваш файл и насколько строго вы понимаете случайность? splitможет, разбить файл на части по 1000 строк в каждой, так что вы получите больше файлов за один вызов sort. Кроме того, вы проверили, если headнемного быстрее, чем tailпотому, что ему не нужно читать весь файл?
Ульрих Шварц
@UlrichSchwarz: Пример файла, который я вставил выше, содержит около 33000 строк. В общем, все файлы моих кроватей будут иметь примерно одинаковое количество рядов. Также, например: из файла с 33000 строк я не хочу получать 33 подмножества (по 1000 строк в каждом) за один прогон. Я только хочу взять лучшие 1000 рядов от каждого пробега. Я также буду делать хвост того же файла. Просто для примера, я использовал headздесь.
biobudhan
Согласно man-странице sort -Rиспользуется «случайный хэш ключей». Создание хэша - это пустая трата времени и, вероятно, занимает больше времени, чем все остальное. Было бы лучше прочитать строки в массив и затем перемешать их с помощью индексов. Лично я бы использовал perlдля этого; Вы можете сделать это, bashно вам понадобится функция для генерации случайных чисел.
Златовласка
@goldilocks: я не perlчеловек! Не могли бы вы помочь мне?
biobudhan
6
Попробуйте shufвместо sort -R, это значительно быстрее. Конечно, выполнение этого в памяти (см. Ответ Perl) побьет все, что требует перечитывания всего файла в оболочке.
frostschutz

Ответы:

14

Предполагая, что у вас достаточно памяти, чтобы сохранить файл, вы можете попробовать

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

Поскольку вы хотите сделать это 10000 раз, я бы порекомендовал интегрировать повторение в скрипт и перетасовывать индексы вместо самого массива, чтобы ускорить процесс:

$ 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

Выше было создано 10000 файлов по 1000 строк в каждом из файла, который содержал 37000 строк (ваш пример файла повторялся 1000 раз). Как видите, в моей системе это заняло чуть больше трех минут.

объяснение

  • use List::Util 'shuffle';: это импортирует модуль Perl, который предоставляет shuffle()функцию, которая рандомизирует массив.
  • @l=<>;: загрузить входной файл ( <>) в массив @l.
  • for $i (1..10000){} : запустить это 10000 раз.
  • @r=shuffle(0..$#l);: $#lэто число элементов в, @lтак @rчто теперь это случайный список порядковых номеров массива @l(строки входного файла).
  • open(my $fh, ">","file.$i.bed");: открыть файл file.$i.bedдля записи. $iбудет принимать значения от 1 до 10000.
  • print $fh @l[@r[0..999]]: возьмите первые 1000 индексов в перемешанном массиве и напечатайте соответствующие строки (элементы @l).

Другой подход заключается в использовании shuf( спасибо @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
Тердон
источник
Вау!! Это замечательно!! Это сработало за 2 минуты :-) У меня есть только еще один вопрос. Как насчет получения последних 1000 строк файла? Потому что нам нужно знать длину (количество строк) в файле, чтобы добиться этого? Пожалуйста помоги!
biobudhan
1
@biobudhan ли рассматривать shufкак предложено frostschutz: for i in {1..10000}; do shuf -n 1000 file.bed > file.$i.bed; done. Это заняло ~ 1 минуту в моей системе. Что касается последних 1000 строк, все, что вам нужно, это tail -n 1000.
Terdon
1
@biobudhan также видит обновленный ответ для 3-кратной более быстрой версии Perl.
Terdon
Да, я попробовал, и теперь работает быстрее! Большое спасибо!!! :-)
biobudhan
Вы дважды проверили выходные файлы версии Perl? Мне кажется странным, что у него так мало sysвремени, которое будет файловым вводом-выводом - оно не должно быть настолько отличным от того shuf, которое имеет ~ 30 с sys. Итак, я протестировал Perl здесь (cut n 'paste) и O_O он создал 1000 файлов, но все файлы были пусты ...
goldilocks
9

Если вы хотите, чтобы тест показал, как быстро это можно сделать, скопируйте его, вставьте 10kshuffle.cppи скомпилируйте g++ 10kshuffle.cpp -o 10kshuffle. Затем вы можете запустить его:

10kshuffle filename < inputfile

Где filenameбазовый путь для файлов вывода; они будут названы filename.0, filename.1и т.д. , и каждая из них содержит первые 1000 строк в случайном порядке. Он записывает имя каждого файла по мере его поступления.

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

На одном ядре с частотой 3,5 ГГц это выполняется за ~ 20 секунд:

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

data.txtбыло 37000 строк, продублированных из вопроса. Если вы хотите, чтобы все выходные данные в выходном файле использовались вместо первых 1000 строк, измените строку 54 на:

for (int j = 0; j < copy.size(); j++) file << copy[j] << endl; 
лютик золотистый
источник
3

Таким образом, в вашем вопросе есть аспект Unix, но сначала стоит решить фундаментальную проблему, а затем попытаться найти способ реализации этого решения в Unix-y.

Вам нужно создать 10 000 образцов размером 1000 из файла с неизвестным большим количеством строк. Это можно сделать за один проход файла, если вы можете хранить 10 000 x 1000 строк в памяти. Если вы не можете хранить столько строк в памяти, вы все равно можете сделать это за один проход, если знаете, сколько строк содержит ваш файл. Если вы не знаете, сколько строк содержит ваш файл, вам потребуется еще один проход для подсчета количества строк.

Алгоритм, в более сложном случае, когда вы не знаете количество строк, заключается в следующем для каждой выборки (параллельно, сохраняя выборки в памяти):

  • включить первые 1000 строк в выборку
  • для n-й строки (где n > 1000) включите ее с вероятностью 1000 / nи отбросьте случайную строку из строк, которые вы уже выбрали. (из-за вероятности сброса некоторых строк нам нужно хранить сэмпл в памяти до конца ввода)

Элегантный способ реализовать второй шаг - генерировать случайное целое число kв [1, n]. Если k <= 1000затем, включите строку и замените существующую kстроку. Вот более стандартное описание алгоритма: http://en.wikipedia.org/wiki/Reservoir_sampling

Если вы знаете количество строк R, то:

  • начать с размера выборки, sиз 0
  • включить n-ю строку с вероятностью (1000 - s) / (R - n + 1)и вывести ее немедленно (и увеличить размер выборки s)

Как это сделать на Unix? awkкажется, ответ на этот пост в Интернете (я не могу ручаться за его правильность, но код есть) https://news.ycombinator.com/item?id=4840043

некромант
источник