File Coverage

blib/lib/Bio/ViennaNGS/BamStat.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             # -*-CPerl-*-
2             # Last changed Time-stamp: <2015-01-05 15:47:13 mtw>
3              
4             package Bio::ViennaNGS::BamStat;
5              
6 1     1   1634 use 5.12.0;
  1         3  
  1         43  
7 1     1   6 use version; our $VERSION = qv('0.12_08');
  1         2  
  1         6  
8 1     1   261 use Bio::DB::Sam 1.39;
  0            
  0            
9             use Moose;
10             use Carp;
11             use Data::Dumper;
12             use namespace::autoclean;
13              
14             has 'bam' => (
15             is => 'rw',
16             isa => 'Str',
17             required => 1,
18             predicate => 'has_bam',
19             );
20              
21             has 'data' => (
22             is => 'ro',
23             isa => 'HashRef',
24             predicate => 'has_data',
25             );
26              
27             has 'control_match' => ( # provides stats how many mapped bases match the reference genome
28             is => 'rw',
29             isa => 'Bool',
30             default => '1',
31             clearer => 'clear_control_match',
32             predicate => 'has_control_match',
33             );
34              
35             has 'control_clip' => ( # provides stats how many bases are soft or hard clipped
36             is => 'rw',
37             isa => 'Bool',
38             default => '1',
39             clearer => 'clear_control_clip',
40             predicate => 'has_control_clip',
41             );
42              
43             has 'control_split' => ( # provides stats how many/often mapped reads are split
44             is => 'rw',
45             isa => 'Bool',
46             default => '1',
47             clearer => 'clear_control_split',
48             predicate => 'has_control_split',
49             );
50              
51             has 'control_qual' => ( # provides stats on quality of the match
52             is => 'rw',
53             isa => 'Bool',
54             default => '1',
55             clearer => 'clear_control_qual',
56             predicate => 'has_control_qual',
57             );
58              
59             has 'control_edit' => ( # provides stats on the edit distance between read and mapped reference
60             is => 'rw',
61             isa => 'Bool',
62             default => '1',
63             clearer => 'clear_control_edit',
64             predicate => 'has_control_edit',
65             );
66              
67             has 'control_flag' => ( # analyses the sam bit flag for qual/strands/pair_vs_single reads
68             is => 'rw',
69             isa => 'Bool',
70             default => '1',
71             clearer => 'clear_control_flag',
72             predicate => 'has_control_flag',
73             );
74              
75             has 'control_score' => ( # provides stats on per-base quality scores
76             is => 'rw',
77             isa => 'Bool',
78             default => '1',
79             clearer => 'clear_control_score',
80             predicate => 'has_control_score',
81             );
82              
83             has 'control_uniq' => ( # gives number and stats of multiplicity of readaligments
84             is => 'rw',
85             isa => 'Bool',
86             default => '1',
87             clearer => 'clear_control_uniq',
88             predicate => 'has_control_uniq',
89             );
90              
91             has 'is_segemehl' => ( # toggles to consider segemehl specific bam feature
92             is => 'rw',
93             isa => 'Bool',
94             default => '0',
95             predicate => 'has_is_segemehl',
96             );
97              
98             has 'data_out' => (
99             is => 'rw',
100             isa => 'HashRef',
101             default => sub { {} },
102             predicate => 'has_data_out',
103             );
104              
105             has 'data_match' => (
106             is => 'rw',
107             isa => 'ArrayRef',
108             default => sub { [] },
109             predicate => 'has_data_match',
110             );
111              
112             has 'data_qual' => (
113             is => 'rw',
114             isa => 'ArrayRef',
115             default => sub { [] },
116             predicate => 'has_data_qual',
117             );
118              
119             has 'data_edit' => (
120             is => 'rw',
121             isa => 'ArrayRef',
122             default => sub { [] },
123             predicate => 'has_data_edit',
124             );
125              
126             has 'data_score' => (
127             is => 'rw',
128             isa => 'ArrayRef',
129             default => sub { [] },
130             predicate => 'has_data_score',
131             );
132              
133             has 'data_clip' => (
134             is => 'rw',
135             isa => 'ArrayRef',
136             default => sub { [] },
137             predicate => 'has_data_clip',
138             );
139              
140             has 'data_split' => (
141             is => 'rw',
142             isa => 'HashRef',
143             default => sub { {} },
144             predicate => 'has_data_split',
145             );
146              
147             has 'data_flag' => (
148             is => 'rw',
149             isa => 'HashRef',
150             default => sub { {} },
151             predicate => 'has_data_flag',
152             );
153              
154             has 'data_uniq' => (
155             is => 'rw',
156             isa => 'HashRef',
157             default => sub { {} },
158             predicate => 'has_data_uniq',
159             );
160              
161             sub stat_singleBam {
162             my ($self) = @_;
163             my $this_function = (caller(0))[3];
164             confess "ERROR [$this_function] Attribute 'bam' not found $!"
165             unless ($self->has_bam);
166              
167             ## read in bam file using Bio::DB::Sam
168             #my $sam = Bio::DB::Sam->new(-bam => $self->bam);
169             my $bam = Bio::DB::Bam->open($self->bam);
170              
171             my $header = $bam->header;
172             my $target_count = $header->n_targets;
173             my @chromosomes = @{$header->target_name};
174             my $target_names = \@chromosomes;
175              
176             while (my $align = $bam->read1) {
177             my %flags = ();
178             my $qname = $align->qname;
179             my $seqid = $target_names->[$align->tid];
180             my $start = $align->pos+1;
181             my $end = $align->calend;
182             my $strand = $align->strand;
183             my $cigar = $align->cigar_str;
184             my @scores = $align->qscore;
185             my $match_qual = $align->qual;
186             my $flag = $align->flag;
187             my $attributes = $align->aux;
188             # print "\$seqid: $seqid\t \$qname: $qname\t $start: $start\t \$end: $end\t \$cigar: $cigar\t\$strand: $strand\t \$match_qual: $match_qual\t \$attributes: $attributes\n";
189             my @tags = $align->get_all_tags;
190             # print ">> tags: @tags\n";
191              
192             #############################
193             ### flag
194             if ($self->has_control_flag){
195              
196             my $multisplit_weight = 1;
197             if ($self->has_is_segemehl && $align->has_tag('XL') ) {
198             $multisplit_weight = (1/($align->aux_get('XL')));
199             }
200              
201             if( $align->get_tag_values('PAIRED')) {
202             ${$self->data_flag}{'paired'}->{'paired-end'}+=$multisplit_weight;
203             }else{
204             ${$self->data_flag}{'paired'}->{'single-end'}+=$multisplit_weight;
205             }
206              
207             if( $align->get_tag_values('REVERSED')) {
208             ${$self->data_flag}{'strand'}->{'reverse'}+=$multisplit_weight;
209             }else{
210             ${$self->data_flag}{'strand'}->{'forward'}+=$multisplit_weight;
211             }
212              
213             if( $align->get_tag_values('PAIRED') && $align->get_tag_values('M_UNMAPPED')){
214             ${$self->data_flag}{'pairs'}->{'unmapped_pair'}+=$multisplit_weight;
215             }
216             elsif( $align->get_tag_values('PAIRED') && !$align->get_tag_values('M_UNMAPPED')) {
217             ${$self->data_flag}{'pairs'}->{'mapped_pair'}+=$multisplit_weight;
218             }
219              
220             if( $align->get_tag_values('DUPLICATE') ){
221             ${$self->data_flag}{'unmapped'}->{'duplicated'}+=$multisplit_weight;
222             }
223             if( $align->get_tag_values('QC_FAILED') ){
224             ${$self->data_flag}{'unmapped'}->{'qual_failed'}+=$multisplit_weight;
225             }
226             if( $align->get_tag_values('UNMAPPED') ){
227             ${$self->data_flag}{'unmapped'}->{'unmapped'}+=$multisplit_weight;
228             }
229             }
230              
231             #############################
232             ### uniq
233             if ($self->has_control_uniq){
234              
235             my $multisplit_weight = 1;
236             if ($self->has_is_segemehl && $align->has_tag('XL') ) {
237             $multisplit_weight = ($align->aux_get('XL'));
238             }
239              
240             my $multimap_weight = 1;
241             if ( $align->has_tag('NH') ) {
242             $multimap_weight = ($align->aux_get('NH'));
243             }
244              
245             ${$self->data_uniq}{'uniq'}+=(1/$multisplit_weight) if($multimap_weight == 1);
246             ${$self->data_uniq}{'mapped'}+=1/($multimap_weight*$multisplit_weight);
247             ${$self->data_uniq}{'multiplicity'}->{$multimap_weight}+=(1/$multisplit_weight);
248              
249             }
250              
251             #############################
252             ### Quality Score read
253             if($self->has_control_qual){
254             if($self->has_is_segemehl && $match_qual == 255){
255             #carp "WARN [$this_function] no match_qual for segemehl";
256             $self->clear_control_qual;
257             }
258             elsif($match_qual){
259             push @{$self->data_qual}, $match_qual;
260             }
261             else{
262             carp "WARN [$this_function] No matchqual for read $qname";
263             carp "WARN [$this_function] Setting \$self->has_control_qual to zero";
264             $self->clear_control_qual;
265             }
266             }
267              
268             #############################
269             #### Edit distance
270             if($self->has_control_edit){
271             if( $align->has_tag('NM') ){
272             push @{$self->data_edit}, $align->aux_get('NM');
273             }
274             else{
275             carp "WARN [$this_function] No <NM> attribute for read $qname";
276             carp "WARN [$this_function] Setting \$self->has_control_edit to zero";
277             $self->clear_control_edit;
278             }
279             }
280              
281             #############################
282             ### Match bases
283             if($self->has_control_match){
284             if($cigar){
285             push @{$self->data_match},
286             sprintf("%.2f", 100*(&cigarmatch($cigar))/(&cigarlength($cigar)));
287             }
288             else{
289             carp "WARN [$this_function] No CIGAR string for read $qname";
290             carp "WARN [$this_function] Setting \$self->has_control_match to zero";
291             $self->clear_control_match;
292             }
293             }
294              
295             #############################
296             ### Clip reads
297             if($self->control_clip){
298             if($cigar){
299             push @{$self->data_clip}, sprintf("%.2f", 100*(&cigarclip($cigar)));
300             }
301             else{
302             carp "WARN [$this_function] No CIGAR string for read $qname";
303             carp "WARN [$this_function] Setting \$self->has_control_clip to zero";
304             $self->clear_control_clip;
305             }
306             }
307              
308             #############################
309             ### Split reads
310             if($self->control_split){
311             if ($self->has_is_segemehl && $align->has_tag('XL') ) {
312             my $split_counts = $align->aux_get('XL');
313             ${$self->data_split}{$split_counts}+=1/($split_counts);
314             ${$self->data_split}{'total'}+=1/($split_counts);
315             }
316             elsif ( $self->has_is_segemehl ){ }
317             else{
318             if($cigar){
319             my $split_counts = cigarsplit($cigar);
320             ${$self->data_split}{$split_counts}++;
321             ${$self->split_data}{'total'}++;
322             }
323             else{
324             carp "WARN [$this_function] No CIGAR string for read $qname";
325             carp "WARN [$this_function] Setting \$self->has_control_split to zero";
326             $self->clear_control_split;
327             }
328             }
329             }
330              
331             } # end while
332              
333              
334             #############################
335             ### Extract reference genome/chromosome sizes from BAM header
336             for (my $i=0;$i<$header->n_targets;$i++){
337             my $seqid = $header->target_name->[$i];
338             my $length = $header->target_len->[$i];
339             ${$self->data_out}{'chrom'}->{$seqid}=$length;
340             }
341              
342             #############################
343             #### uniq
344             if ($self->has_control_uniq){
345             ${$self->data_out}{'uniq'}->{'uniq_mapped_reads'}=${$self->data_uniq}{'uniq'};
346             ${$self->data_out}{'uniq'}->{'mapped_reads'}=sprintf("%d", ${$self->data_uniq}{'mapped'});
347             ${$self->data_out}{'uniq'}->{'uniq_mapped_reads_percent'}=
348             sprintf("%.2f", (100*${$self->data_uniq}{'uniq'}/${$self->data_uniq}{'mapped'}));
349             foreach my $multiplicity (sort {$a cmp $b} keys %{${$self->data_uniq}{'multiplicity'}}) {
350             ${$self->data_out}{'uniq'}->{'distribution_of_multimapper'}->{"<$multiplicity>"}=
351             (${$self->data_uniq}{'multiplicity'}->{$multiplicity}/$multiplicity);
352             }
353             }
354              
355             #############################
356             ##### quality stats
357             if($self->has_control_qual){
358             my %qual_stats=%{stats(@${$self->data_qual})};
359             if ($qual_stats{'min'} == 255 && $qual_stats{'max'} == 255){
360             carp "WARN [$this_function] No matchqual (all values set to 255)";
361             carp "WARN [$this_function] Setting \$self->has_control_qual to zero";
362             $self->clear_control_qual;
363             }
364             else{
365             ${$self->data_out}{'qual'}->{'min'} = $qual_stats{'min'};
366             ${$self->data_out}{'qual'}->{'1q'} = $qual_stats{'1q'};
367             ${$self->data_out}{'qual'}->{'mean'} = $qual_stats{'mean'};
368             ${$self->data_out}{'qual'}->{'med'} = $qual_stats{'med'};
369             ${$self->data_out}{'qual'}->{'3q'} = $qual_stats{'3q'};
370             ${$self->data_out}{'qual'}->{'max'} = $qual_stats{'max'};
371             }
372             }
373              
374             #############################
375             ### Clip data
376             if($self->has_control_clip){
377             my %clip_stats=%{stats(@{$self->data_clip})};
378              
379             ${$self->data_out}{'clip'}->{'min'} = $clip_stats{'min'};
380             ${$self->data_out}{'clip'}->{'1q'} = $clip_stats{'1q'};
381             ${$self->data_out}{'clip'}->{'mean'} = $clip_stats{'mean'};
382             ${$self->data_out}{'clip'}->{'med'} = $clip_stats{'med'};
383             ${$self->data_out}{'clip'}->{'3q'} = $clip_stats{'3q'};
384             ${$self->data_out}{'clip'}->{'max'} = $clip_stats{'max'};
385             }
386              
387             #############################
388             ##### Match data
389             if($self->has_control_match){
390             my %match_stats=%{stats(@{$self->data_match})};
391              
392             ${$self->data_out}{'match'}->{'min'} = $match_stats{'min'};
393             ${$self->data_out}{'match'}->{'1q'} = $match_stats{'1q'};
394             ${$self->data_out}{'match'}->{'mean'} = $match_stats{'mean'};
395             ${$self->data_out}{'match'}->{'med'} = $match_stats{'med'};
396             ${$self->data_out}{'match'}->{'3q'} = $match_stats{'3q'};
397             ${$self->data_out}{'match'}->{'max'} = $match_stats{'max'};
398             }
399              
400             #############################
401             ##### Edit distance
402             if($self->has_control_edit){
403             my %edit_stats=%{stats(@{$self->data_edit})};
404              
405             ${$self->data_out}{'edit'}->{'min'} = $edit_stats{'min'};
406             ${$self->data_out}{'edit'}->{'1q'} = $edit_stats{'1q'};
407             ${$self->data_out}{'edit'}->{'mean'} = $edit_stats{'mean'};
408             ${$self->data_out}{'edit'}->{'med'} = $edit_stats{'med'};
409             ${$self->data_out}{'edit'}->{'3q'} = $edit_stats{'3q'};
410             ${$self->data_out}{'edit'}->{'max'} = $edit_stats{'max'};
411             }
412              
413             #############################
414             ###### flags
415             if ($self->has_control_flag){
416              
417             ${$self->data_flag}{'pairs'}->{'mapped_pair'} = 0
418             unless ( defined(${$self->data_flag}{'pairs'}->{'mapped_pair'}) );
419             ${$self->data_flag}{'pairs'}->{'unmapped_pair'} = 0
420             unless ( defined(${$self->data_flag}{'pairs'}->{'unmapped_pair'}) );
421              
422             ${$self->data_flag}{'paired'}->{'paired-end'} = 0
423             unless ( defined(${$self->data_flag}{'paired'}->{'paired-end'}) );
424             ${$self->data_flag}{'paired'}->{'single-end'} = 0
425             unless ( defined(${$self->data_flag}{'paired'}->{'single-end'}) );
426              
427             my $total_mapped = ${$self->data_flag}{'paired'}->{'paired-end'} +
428             ${$self->data_flag}{'paired'}->{'single-end'};
429              
430             ${$self->data_out}{'aln_count'}->{'mapped_pair'} =
431             ${$self->data_flag}{'pairs'}->{'mapped_pair'};
432             ${$self->data_out}{'aln_count'}->{'unmapped_pair'} =
433             ${$self->data_flag}{'pairs'}->{'unmapped_pair'};
434             ${$self->data_out}{'aln_count'}->{'mapped_single'} =
435             ${$self->data_flag}{'paired'}->{'single-end'};
436             ${$self->data_out}{'aln_count'}->{'total'} = $total_mapped;
437              
438             ${$self->data_flag}{'strand'}->{'forward'} = 0
439             unless ( defined(${$self->data_flag}{'strand'}->{'forward'}) );
440             ${$self->data_flag}{'strand'}->{'reverse'} = 0
441             unless ( defined(${$self->data_flag}{'strand'}->{'reverse'}) );
442              
443             my $total_strands = ${$self->data_flag}{'strand'}->{'forward'} +
444             ${$self->data_flag}{'strand'}->{'reverse'};
445              
446             ${$self->data_out}{'strand'}->{'forward'} = ${$self->data_flag}{'strand'}->{'forward'};
447             ${$self->data_out}{'strand'}->{'reverse'} = ${$self->data_flag}{'strand'}->{'reverse'};
448             }
449              
450             #############################
451             ###### split
452             if ($self->has_control_split){
453             foreach my $splits (sort {$a cmp $b} keys %{$self->data_split}) {
454             my $split_counts=( defined(${$self->data_split}{$splits}) )?(${$self->data_split}{$splits}):(0);
455             ${$self->data_out}{'split'}->{'distribution_of_multisplit'}->{"$splits"}=$split_counts;
456             }
457             }
458              
459             }
460              
461             __PACKAGE__->meta->make_immutable;
462              
463              
464             no Moose;
465              
466             sub stats{
467             # usage: %h = %{stats(@a)};
468             my @vals = sort {$a <=> $b} @_;
469             my %stats = ();
470             my $median = '';
471            
472             if(@vals%2){$stats{'med'} = $vals[int(@vals/2)];} #odd median
473             else{$stats{'med'} = ($vals[int(@vals/2)-1] + $vals[int(@vals/2)])/2;} #even median
474             $stats{'med'} = sprintf("%.2f", $stats{'med'});
475             $stats{'mean'} = sprintf("%.2f", &mean(\@vals)); ## mean
476             $stats{'min'} = sprintf("%.2f", &min(\@vals)); ## min
477             $stats{'max'} = sprintf("%.2f", &max(\@vals)); ## max
478             $stats{'1q'} = sprintf("%.2f", $vals[int(@vals/4)]); ## 1.quartile
479             $stats{'3q'} = sprintf("%.2f", $vals[int((@vals*3)/4)]); ## 3.quartile
480              
481             return(\%stats);
482             }
483              
484             sub mean { # usage: $h = %{mean(\@a)};
485             my ($arrayref) = @_;
486             my $sum;
487             foreach (@$arrayref) {$sum += $_}
488             return $sum / @$arrayref;
489             }
490              
491             sub max { # usage: $h = %{max(\@a)};
492             my ($arrayref) = @_;
493             my $max = $arrayref->[0];
494             foreach (@$arrayref) {$max = $_ if $_ > $max}
495             return $max;
496             }
497              
498             sub min { # usage: $h = %{min(\@a)};
499             my ($arrayref) = @_;
500             my $min = $arrayref->[0];
501             foreach (@$arrayref) {$min = $_ if $_ < $min}
502             return $min;
503             }
504              
505             sub cigarlength { #usage: &cigarlength($cigarstring)
506             my $cigar_string = shift;
507             my $cigar_length = 0;
508             while($cigar_string=~m/(\d+)[MIX=]/g){$cigar_length+=$1}
509             return($cigar_length);
510             }
511              
512             sub cigarmatch { #usage: cigarmatch($cigarstring)
513             my $cigar_string = shift;
514             my $cigar_match = 0;
515             while($cigar_string=~m/(\d+)[M=]/g){$cigar_match+=$1}
516             return($cigar_match);
517             }
518              
519             sub cigarsplit { #usage: cigarsplit($cigarstring)
520             my $cigar_string = shift;
521             my $cigar_split = 0;
522             while($cigar_string=~m/N/g){$cigar_split+=1}
523             return($cigar_split);
524             }
525              
526             sub cigarclip { #usage: cigarclip($cigarstring)
527             my $cigar_string = shift;
528             my $cigar_length = 0;
529             my $cigar_clip = 0;
530             while($cigar_string=~m/(\d+)[SH]/g) {$cigar_clip+=$1}
531             while($cigar_string=~m/(\d+)\D/g){$cigar_length+=$1}
532             return ($cigar_length==0)?(0):($cigar_clip/$cigar_length);
533             }
534              
535              
536              
537             1;
538             __END__
539              
540              
541             =head1 NAME
542              
543             Bio::ViennaNGS::BamStat - Moose interface to BAM mapping statistics
544              
545             =head1 SYNOPSIS
546              
547             use Bio::ViennaNGS::BamStat;
548              
549             my $bss1 = Bio::ViennaNGS::BamStat->new(bam => "path/to/file.bam");
550              
551             =head1 DESCRIPTION
552              
553             This module provides a L<Moose> interface to the mapping statistics of
554             a single BAM file. It builds on L<Bio::DB::Sam> and
555              
556             =head1 SEE ALSO
557              
558             =over
559              
560             =item L<Bio::ViennaNGS>
561              
562             =back
563              
564             =head1 AUTHORS
565              
566             =over
567              
568             =item Fabian Amman E<lt>fabian@tbi.univie.ac.atE<gt>
569              
570             =item Michael T. Wolfinger E<lt>michael@wolfinger.euE<gt>
571              
572             =back
573              
574             =head1 COPYRIGHT AND LICENSE
575              
576             Copyright (C) 2014 by Michael T. Wolfinger
577              
578             This library is free software; you can redistribute it and/or modify
579             it under the same terms as Perl itself, either Perl version 5.16.3 or,
580             at your option, any later version of Perl 5 you may have available.
581              
582             This software is distributed in the hope that it will be useful, but
583             WITHOUT ANY WARRANTY; without even the implied warranty of
584             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
585              
586             =cut