perlhashrefperl-hash

Merging N no of files based on their first column in perl


My question is similar to this question posted earlier.

I am having many files which I need to merge them based on the presence or absence of the first column ID, but while merging I am getting lots of empty values in my output file, I want those empty values to be zero if it is not present in another file. The example below is based on only two files content, but I have many sample files like this format (tabular).

For example:

File1
ID   Value
123  1
231  2
323  3
541  7
File2
ID   Value
541  6
123  1
312  3
211  4
Expected Output:
ID    File1    File2
123    1       1
231    2       0
323    3       0
541    7       6
312    0       3
211    0       4

Obtaining Output:
ID    File1    File2
123    1       1
231    2       
323    3       
541    7       6
312    undef   3
211    undef   4

As you can see above I am getting output but in file2 column, it's not adding zero or leaving empty and in case of file1 column it is having undef value. I have checked undef values and then my final output gives zeros in place of undef values but still I am having those empty spaces. Please find my code below (hardcoded only for two files).

    #!/usr/bin/perl
    use strict;
    use warnings;
    use diagnostics;
    use Data::Dumper;
    my $path = "/home/pranjay/Projects/test";
    my @files = ("s1.txt","s2.txt");
    my %classic_com;
    my $cnt;
    my $classic_txt;
    my $sample_cnt = 0;
    my $classic_txtcomb = "test_classic.txt";
    open($classic_txt,">$path/$classic_txtcomb") or die "Couldn't open file 
    $classic_txtcomb for writing,$!";

    print $classic_txt "#ID\t"."file1\tfile2\n";

    foreach my $file(@files){
    $sample_cnt++;
    print "$sample_cnt\n";
    open($cnt,"<$path/$file")or die "Couldn't open file $file for reading,$!";
    while(<$cnt>){

            chomp($_);
            my @count = ();
            next if($_=~/^ID/);
            my @record=();
            @record=split(/\t/,$_);
            my $scnt = $sample_cnt -1;
            if((exists($classic_com{$record[0]})) and ($sample_cnt > 0)){
                    ${$classic_com{$record[0]}}[$scnt]=$record[1];
            }else{
                    $count[$scnt] = "$record[1]";
                    $classic_com{$record[0]}= [@count];
            }
        }
    }


my %final_txt=();
    foreach my $key ( keys %classic_com ) {
            #print "$key: ";
            my @val = @{ $classic_com{$key} };
            my @v;
            foreach my $i ( @val ) {
                    if(not defined($i)){
                            $i = 0;
                            push(@v, $i);
                    }else{
                            push(@v, $i);
                            next;
                    }
            }
            $final_txt{$key} = [@v];
    }
    #print Dumper %classic_com;
    while(my($key,$value)=each(%final_txt)){
            my $val=join("\t", @{$value});
            print $classic_txt "$key\t"."@{$value}"."\n";
    }

Solution

  • Just read the input files into a hash of arrays. The topmost key is the ID, each inner array contains the value for file i on the i-th position. When printing, use the // defined-or operator to replace undefs with zeroes:

    #!/usr/bin/perl
    use warnings;
    use strict;
    use feature qw{ say };
    
    my %merged;
    
    my $file_tally = 0;
    while (my $file = shift) {
        open my $in, '<', $file or die "$file: $!";
        <$in>;  # skip the header
        while (<$in>) {
            my ($id, $value) = split;
            $merged{$id}[$file_tally] = $value;
        }
        ++$file_tally;
    }
    
    for my $value (keys %merged) {
        my @values = @{ $merged{$value} };
        say join "\t", $value, map $_ // 0, @values[0 .. $file_tally - 1];
    }