Швидкий підхід до сортування даних


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

1
Наскільки великий ваш файл і наскільки суворе ваше поняття "випадковий"? splitможе, помилка, розділити файл на шматки по 1000 рядків кожен, так що ви отримаєте більше файлів за один виклик sort. Крім того, чи перевірили ви, чи headтрохи швидше, ніж tailчерез те, що не потрібно читати весь файл?
Ульріх Шварц

@UlrichSchwarz: Прикладний файл, який я вставив вище, містить близько 33000 рядків. Взагалі всі мої файли ліжка матимуть більш-менш однакову кількість рядків. Також, наприклад: з 33000 рядкових файлів я не хочу отримувати 33 підмножини (1000 рядків у кожному) за один запуск. Я хочу лише взяти перші 1000 рядків з кожного прогону. Я також буду робити хвіст з того ж файлу. Просто для зразка я headтут використовував .
біобудхан

За словами man, сторінка sort -Rвикористовує "випадковий хеш ключів". Створення хешу - це загальна витрата часу і, ймовірно, займає більше часу, ніж усе інше. Було б краще прочитати рядки в масив, а потім перемістити їх, використовуючи індекси. Особисто я би скористався perlцим; ви можете це зробити, bashале вам потрібна функція для генерування випадкових чисел.
goldilocks

@goldilocks: Я не perlлюдина! Не могли б ви допомогти мені?
біобудхан

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 рядків файлу? Тому що нам потрібно знати довжину (кількість рядків) у файлі, щоб досягти цього? Будь ласка, допоможіть!
біобудхан

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

Так, я спробував це, і він працює швидше зараз !! Велике спасибі!!! :-)
біобудхан

Ви двічі перевірили вихідні файли версії perl? Мені здається дивним, що у нього так мало sysчасу, який би був файл вводу / виводу - це не повинно бути настільки іншим, ніж той shuf, який має ~ 30 років sys. Отож, я перевірив перл тут (вирізати "вставити"), і 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 х 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.