Sắp xếp dữ liệu tiếp cận nhanh hơn


11

Tôi cần sắp xếp một bedtệp ngẫu nhiên 10000 lần và lấy 1000 hàng hàng đầu mỗi lần. Hiện tại, tôi đang sử dụng đoạn mã sau:

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

Phải mất gần 6 giờ để làm điều này cho mỗi tệp. Tôi có khoảng 150 người trong số họ sẽ được giải quyết. Có một giải pháp nhanh hơn cho việc này?

Một mẫu dữ liệu (myfile.bed_sort) tôi có:

    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
Tệp của bạn lớn như thế nào và khái niệm "ngẫu nhiên" của bạn nghiêm ngặt đến mức nào? splitcó thể, sai, chia một tệp thành 1000 dòng mỗi dòng, do đó bạn sẽ nhận được nhiều tệp hơn trong một cuộc gọi sort. Ngoài ra, bạn đã kiểm tra nếu headnhanh hơn một chút so với tailvì nó không cần phải đọc qua toàn bộ tệp?
Ulrich Schwarz

@UlrichSchwarz: Tệp mẫu mà tôi đã dán ở trên có khoảng 33000 hàng. Nói chung, tất cả các tập tin giường của tôi sẽ có ít nhiều cùng một số hàng. Ngoài ra, ví dụ: từ tệp 33000 hàng, tôi không muốn nhận 33 tập con (mỗi hàng 1000) trong một lần chạy. Tôi chỉ muốn lấy 1000 hàng đầu từ mỗi lần chạy. Tôi cũng sẽ làm một cái đuôi của cùng một tập tin. Chỉ cho mẫu, tôi sử dụng headở đây.
biobudhan

Theo trang man sort -Rsử dụng "hàm băm ngẫu nhiên". Tạo băm là một sự lãng phí hoàn toàn thời gian và có thể mất nhiều thời gian hơn bất cứ điều gì khác. Sẽ tốt hơn nếu đọc các dòng thành một mảng và sau đó xáo trộn bằng cách sử dụng các chỉ mục. Cá nhân, tôi sẽ sử dụng perlcho điều đó; bạn có thể làm điều đó với bashnhưng bạn sẽ cần một hàm để tạo các số ngẫu nhiên.
goldilocks

@goldilocks: Tôi không phải là perlngười! Ông có thể giúp tôi không?
biobudhan

6
Hãy thử shufthay vì sort -R, nó nhanh hơn đáng kể. Tất nhiên, thực hiện nó trong bộ nhớ (xem câu trả lời của Perl) sẽ đánh bại bất cứ thứ gì yêu cầu đọc lại toàn bộ tệp trong trình bao.
frostschutz

Câu trả lời:


14

Giả sử bạn có đủ bộ nhớ để nhét tệp, bạn có thể thử

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

Vì bạn muốn thực hiện điều này 10000 lần, tôi khuyên bạn nên tích hợp sự lặp lại vào tập lệnh và xáo trộn các chỉ số thay vì chính mảng để tăng tốc mọi thứ:

$ 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

Ở trên đã tạo ra 10000 tệp gồm 1000 dòng mỗi dòng từ một tệp chứa 37000 hàng (tệp ví dụ của bạn được lặp lại 1000 lần). Như bạn có thể thấy, nó mất hơn ba phút trên hệ thống của tôi.

Giải trình

  • use List::Util 'shuffle';: cái này nhập một mô-đun Perl cung cấp shuffle()hàm ngẫu nhiên một mảng.
  • @l=<>;: tải tệp đầu vào ( <>) vào mảng @l.
  • for $i (1..10000){} : chạy 10000 lần này.
  • @r=shuffle(0..$#l);: $#llà số phần tử trong @lvì vậy @rbây giờ là danh sách ngẫu nhiên các số chỉ mục của mảng @l(dòng của tệp đầu vào).
  • open(my $fh, ">","file.$i.bed");: mở một tệp được gọi file.$i.bedđể viết. $isẽ lấy các giá trị từ 1 đến 10000.
  • print $fh @l[@r[0..999]]: lấy 1000 chỉ mục đầu tiên trong mảng được xáo trộn và in các dòng (phần tử của @l) tương ứng .

Một cách tiếp cận khác là sử dụng shuf( cảm ơn @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

Chà !! Thật tuyệt vời !! Nó hoạt động trong 2 phút :-) Tôi chỉ còn một câu hỏi nữa. Làm thế nào về việc cũng lấy 1000 dòng cuối cùng của tập tin? Bởi vì chúng ta cần biết độ dài (số dòng) trong tệp để đạt được điều này? Xin vui lòng giúp đỡ!
biobudhan

1
@biobudhan xem xét shuftheo đề xuất của frostschutz : for i in {1..10000}; do shuf -n 1000 file.bed > file.$i.bed; done. Điều đó mất ~ 1 phút trên hệ thống của tôi. Đối với 1000 dòng cuối cùng, tất cả những gì bạn cần là tail -n 1000.
terdon

1
@biobudhan cũng thấy câu trả lời cập nhật cho phiên bản perl nhanh hơn gấp 3 lần.
terdon

Vâng, tôi đã thử nó và nó hoạt động nhanh hơn bây giờ !! Cảm ơn rât nhiều!!! :-)
biobudhan

Bạn đã kiểm tra kỹ các tập tin đầu ra của phiên bản perl chưa? Nó có vẻ kỳ lạ với tôi rằng nó có rất ít systhời gian, đó sẽ là tệp I / O - điều này không nên quá khác biệt so với shufcái có ~ 30 giây sys. Vì vậy, tôi đã thử nghiệm perl one ở đây (cắt n 'paste) và O_O nó đã tạo ra 1000 tệp nhưng tất cả các tệp đều trống rỗng ...
goldilocks

9

Nếu bạn muốn một điểm chuẩn để xem nó có thể được thực hiện nhanh như thế nào, hãy sao chép và dán nó vào 10kshuffle.cppvà biên dịch g++ 10kshuffle.cpp -o 10kshuffle. Sau đó bạn có thể chạy nó:

10kshuffle filename < inputfile

Đâu filenamelà đường dẫn cơ sở để sử dụng cho các tệp đầu ra; chúng sẽ được đặt tên filename.0, filename.1v.v. và mỗi dòng chứa 1000 dòng xáo trộn đầu tiên. Nó viết tên của mỗi tập tin khi nó đi.

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

Trên một lõi 3,5 Ghz, thời gian này chạy trong ~ 20 giây:

   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được 37000 dòng trùng lặp từ câu hỏi. Nếu bạn muốn toàn bộ xáo trộn trong tệp đầu ra thay vì 1000 dòng đầu tiên, hãy thay đổi dòng 54 thành:

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

3

Vì vậy, có một khía cạnh Unix cho câu hỏi của bạn, nhưng nó đáng để giải quyết vấn đề cơ bản của bạn trước và sau đó cố gắng tìm một cách Unix-y để thực hiện giải pháp đó.

Bạn cần tạo 10.000 mẫu có kích thước 1.000 mỗi mẫu từ một tệp có số lượng lớn hàng không xác định. Có thể thực hiện việc này trong một lần duy nhất của tệp nếu bạn có thể giữ 10.000 x 1.000 hàng trong bộ nhớ. Nếu bạn không thể giữ nhiều hàng trong bộ nhớ, bạn vẫn có thể thực hiện trong một lần duy nhất nếu bạn biết tệp của mình chứa bao nhiêu hàng. Nếu bạn không biết có bao nhiêu hàng thì tệp của bạn chứa một lượt bổ sung để đếm số lượng hàng.

Thuật toán, trong trường hợp khó khăn hơn khi bạn không biết số lượng hàng, là thực hiện các thao tác sau cho từng mẫu (song song, giữ các mẫu trong bộ nhớ):

  • bao gồm 1.000 hàng đầu tiên trong mẫu
  • đối với hàng thứ n (trong đó n > 1000), hãy đưa nó vào xác suất 1000 / nvà loại bỏ một hàng ngẫu nhiên khỏi các hàng bạn đã chọn. (vì khả năng loại bỏ một số hàng, chúng tôi cần giữ mẫu trong bộ nhớ cho đến khi kết thúc đầu vào)

Một cách thanh lịch để thực hiện bước thứ hai là để tạo ra một số nguyên ngẫu nhiên ktrong [1, n]. Nếu k <= 1000sau đó bao gồm hàng và thay thế khàng -th hiện có với nó. Dưới đây là mô tả chuẩn hơn về thuật toán: http://en.wikipedia.org/wiki/Reservoir_sampling

Nếu bạn biết số lượng hàng R, thì:

  • bắt đầu với cỡ mẫu, sbằng 0
  • bao gồm hàng thứ n với xác suất (1000 - s) / (R - n + 1)và xuất nó ngay lập tức (và tăng kích thước mẫu s)

Làm thế nào để làm điều này trên Unix? awkdường như là câu trả lời cho mỗi bài đăng này trên Internet (tôi không thể đảm bảo tính chính xác của nó, nhưng mã ở đó) https://news.ycombinator.com/item?id=4840043

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.