Menyortir data dengan pendekatan yang lebih cepat

11

Saya perlu mengurutkan bedfile secara acak 10.000 kali dan mengambil 1000 baris teratas setiap kali. Saat ini, saya menggunakan kode berikut:

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

Butuh hampir 6 jam untuk melakukan ini untuk setiap file. Saya memiliki sekitar 150 di antaranya untuk dikerjakan. Apakah ada solusi yang lebih cepat untuk ini?

Contoh data (myfile.bed_sorted) Saya punya:

    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
sumber
1
Seberapa besar file Anda dan seberapa ketat gagasan Anda tentang "acak"? splitdapat, err, membagi file menjadi potongan 1000 baris masing-masing, sehingga Anda akan mendapatkan lebih banyak file dalam satu panggilan sort. Juga, sudahkah Anda memeriksa jika headsedikit lebih cepat daripada tailkarena tidak perlu membaca seluruh file?
Ulrich Schwarz
@UlrichSchwarz: File sampel yang saya tempel di atas berisi sekitar 33000 baris. Secara umum, semua file tempat tidur saya akan memiliki jumlah baris yang kurang lebih sama. Juga misalnya: dari file 33000 baris, saya tidak ingin mendapatkan 33 subset (1000 baris di masing-masing) dalam sekali jalan. Saya hanya ingin mengambil 1000 baris teratas dari setiap putaran. Saya juga akan melakukan ekor file yang sama. Hanya untuk sampel, saya gunakan di headsini.
biobudhan
Menurut halaman manual sort -Rmenggunakan "hash kunci acak". Membuat hash adalah buang-buang waktu dan mungkin membutuhkan waktu lebih lama dari yang lainnya. Akan lebih baik untuk membaca baris menjadi array dan kemudian mengocoknya menggunakan indeks. Secara pribadi, saya akan gunakan perluntuk itu; Anda bisa melakukannya dengan bashtetapi Anda akan membutuhkan fungsi untuk menghasilkan angka acak.
goldilocks
@goldilocks: Saya bukan perlorang! Bisakah Anda membantu saya?
biobudhan
6
Coba shufalih-alih sort -R, ini jauh lebih cepat. Tentu saja, melakukannya di memori (lihat Perl jawaban) akan mengalahkan apa pun yang mengharuskan membaca ulang seluruh file di shell.
frostschutz

Jawaban:

14

Dengan asumsi Anda memiliki cukup memori untuk menyeruput file, Anda bisa mencoba

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

Karena Anda ingin melakukan ini sebanyak 10.000 kali, saya akan merekomendasikan untuk mengintegrasikan pengulangan ke dalam skrip dan mengacak indeks daripada array itu sendiri untuk mempercepat:

$ 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

Di atas menciptakan 10.000 file 1000 baris masing-masing dari file yang berisi 37000 baris (file contoh Anda diulang 1000 kali). Seperti yang Anda lihat, butuh sedikit lebih dari tiga menit pada sistem saya.

Penjelasan

  • use List::Util 'shuffle';: ini mengimpor modul Perl yang menyediakan shuffle()fungsi yang mengacak array.
  • @l=<>;: memuat file input ( <>) ke dalam array @l.
  • for $i (1..10000){} : jalankan 10000 kali ini.
  • @r=shuffle(0..$#l);: $#ladalah jumlah elemen dalam @ljadi @rsekarang daftar acak nomor indeks array @l(baris file input).
  • open(my $fh, ">","file.$i.bed");: buka file yang dipanggil file.$i.beduntuk menulis. $iakan mengambil nilai dari 1 hingga 10.000.
  • print $fh @l[@r[0..999]]: ambil 1000 indeks pertama dalam array acak dan cetak baris yang sesuai (elemen @l).

Pendekatan lain adalah menggunakan shuf( terima kasih @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
sumber
Wow!! Itu mengagumkan!! Ini bekerja dalam 2 menit :-) Saya punya satu pertanyaan lagi. Bagaimana kalau juga mengambil 1000 baris terakhir file? Karena Kita perlu tahu panjang (jumlah baris) dalam file untuk mencapai ini? Tolong bantu!
biobudhan
1
@biobudhan jangan menganggap shufseperti yang disarankan oleh frostschutz: for i in {1..10000}; do shuf -n 1000 file.bed > file.$i.bed; done. Butuh ~ 1 menit pada sistem saya. Adapun 1000 baris terakhir, yang Anda butuhkan adalah tail -n 1000.
terdon
1
@biobudhan juga melihat jawaban yang diperbarui untuk versi perl 3x lebih cepat.
terdon
Ya, saya mencobanya dan berfungsi lebih cepat sekarang !! Terima kasih banyak!!! :-)
biobudhan
Apakah Anda memeriksa file output versi perl? Tampaknya aneh bagi saya bahwa ini memiliki sedikit syswaktu, yang akan menjadi file I / O - ini seharusnya tidak begitu berbeda dari yang shufada, yang memiliki ~ 30-an sys. Jadi saya menguji perl satu di sini (cut n 'paste) dan O_O itu menciptakan 1000 file tetapi semua file kosong ...
goldilocks
9

Jika Anda ingin tolok ukur untuk melihat seberapa cepat dapat dilakukan, salin tempel ini ke dalam 10kshuffle.cppdan kompilasi g++ 10kshuffle.cpp -o 10kshuffle. Anda kemudian dapat menjalankannya:

10kshuffle filename < inputfile

Di mana filenamepath dasar untuk digunakan untuk file output; mereka akan dinamai filename.0,, filename.1dll. dan masing-masing berisi 1000 baris pertama dari shuffle. Itu menulis nama setiap file saat berjalan.

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

Pada inti 3,5 Ghz tunggal, ini berjalan dalam ~ 20 detik:

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

data.txtadalah 37.000 baris digandakan dari pertanyaan. Jika Anda ingin seluruh shuffle dalam file output daripada 1000 baris pertama, ubah baris 54 ke:

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

Jadi ada aspek Unix untuk pertanyaan Anda, tetapi ada baiknya memecahkan masalah mendasar Anda terlebih dahulu dan kemudian mencoba menemukan cara Unix-y untuk mengimplementasikan solusi itu.

Anda perlu membuat 10.000 sampel berukuran 1.000 masing-masing dari file dengan jumlah baris yang tidak diketahui. Dimungkinkan untuk melakukan ini dalam satu lintasan file jika Anda dapat menyimpan 10.000 x 1.000 baris dalam memori. Jika Anda tidak dapat menahan banyak baris dalam memori, Anda masih dapat melakukannya dalam satu pass tunggal jika Anda tahu berapa banyak baris file Anda. Jika Anda tidak tahu berapa banyak baris file Anda, Anda perlu satu pass tambahan untuk menghitung jumlah baris.

Algoritme, dalam kasus yang lebih sulit ketika Anda tidak tahu jumlah baris, adalah melakukan hal berikut untuk setiap sampel (secara paralel, menyimpan sampel dalam memori):

  • termasuk 1.000 baris pertama dalam sampel
  • untuk baris ke-n (di mana n > 1000), sertakan dengan probabilitas 1000 / ndan buang baris acak dari baris yang telah Anda pilih. (karena kemungkinan membuang beberapa baris, kami perlu menyimpan sampel dalam memori sampai akhir input)

Cara elegan untuk menerapkan langkah kedua adalah menghasilkan bilangan bulat acak kdi [1, n]. Jika k <= 1000kemudian sertakan baris dan ganti baris yang ada kdengan itu. Berikut ini deskripsi yang lebih standar tentang algoritme: http://en.wikipedia.org/wiki/Reservoir_sampling

Jika Anda tahu jumlah baris R, maka:

  • mulai dengan ukuran sampel, sdari 0
  • sertakan baris ke-n dengan probabilitas (1000 - s) / (R - n + 1)dan hasilkan segera (dan menambah ukuran sampel s)

Bagaimana melakukan ini di Unix? awktampaknya menjadi jawaban per posting ini di Internet (saya tidak bisa menjamin kebenarannya, tetapi kodenya ada di sana) https://news.ycombinator.com/item?id=4840043

ahli nujum
sumber