File Coverage

blib/lib/Bio/Gonzales/SummarizedExperiment.pm
Criterion Covered Total %
statement 310 501 61.8
branch 66 152 43.4
condition 67 162 41.3
subroutine 45 91 49.4
pod 49 54 90.7
total 537 960 55.9


line stmt bran cond sub pod time code
1             package Bio::Gonzales::SummarizedExperiment;
2              
3 1     1   153350 use warnings;
  1         12  
  1         35  
4 1     1   6 use strict;
  1         2  
  1         20  
5 1     1   615 use utf8;
  1         15  
  1         5  
6 1     1   32 use Carp;
  1         1  
  1         54  
7              
8 1     1   12 use v5.11;
  1         4  
9 1     1   5 use IO::Handle ();
  1         2  
  1         19  
10              
11 1     1   603 use List::MoreUtils qw/firstidx indexes any/;
  1         13344  
  1         5  
12 1     1   1298 use List::Util qw/max/;
  1         3  
  1         71  
13 1     1   494 use Bio::Gonzales::Matrix::IO qw/mspew mslurp/;
  1         5  
  1         84  
14 1     1   1467 use Algorithm::Loops qw/MapCarU/;
  1         3624  
  1         90  
15              
16 1     1   27 use Bio::Gonzales::Matrix::Util;
  1         6  
  1         62  
17              
18 1     1   10 use Data::Dumper;
  1         3  
  1         54  
19 1     1   1055 use JSON::XS;
  1         7120  
  1         80  
20              
21 1     1   646 use Clone;
  1         3508  
  1         87  
22              
23             # Imports hier
24              
25 1     1   618 use Moo;
  1         12089  
  1         5  
26 1     1   2182 use namespace::clean;
  1         12045  
  1         6  
27              
28             our $VERSION = 0.01_01;
29             our $NA_VALUE = 'NA';
30              
31             has [qw/assay col_data row_data row_names col_names row_data_names col_data_names meta_data/] =>
32             ( is => 'rw', default => sub { [] } );
33              
34             has na_value => ( is => 'rw', default => sub { $NA_VALUE } );
35              
36 0     0 1 0 sub data { shift->assay(@_) }
37              
38 0     0 1 0 sub header { shift->col_names(@_) }
39              
40             sub slurp_assay {
41 0     0 1 0 my $class = shift;
42              
43 0         0 my ( $m, $cn, $rn ) = mslurp(@_);
44 0   0     0 $cn //= [];
45 0   0     0 $rn //= [];
46 0         0 return $class->new( assay => $m, col_names => $cn, row_names => $rn );
47             }
48              
49             sub spew_assay {
50 0     0 1 0 my $self = shift;
51 0         0 my $src = shift;
52 0   0     0 my $param = shift // {};
53 0         0 my %c = %$param;
54 0 0 0     0 $c{header} = $self->col_names if ( $param->{header} || $param->{col_names} );
55 0 0       0 $c{row_names} = $self->row_names if ( $param->{row_names} );
56 0 0       0 $c{col_data} = $self->col_data if ( $param->{col_data} );
57 0         0 $c{na_value} = $self->na_value;
58 0         0 mspew( $src, $self->assay, \%c );
59 0         0 return $self;
60             }
61              
62             sub _reorder {
63 4     4   8 my $self = shift;
64 4         4 my $idcs = shift;
65              
66 4         11 my $assay = $self->assay;
67 4         8 my $row_names = $self->row_names;
68 4         8 my $row_data = $self->row_data;
69 4         7 my $row_data_names = $self->row_data_names;
70              
71 4         7 $self->assay( [ @{$assay}[@$idcs] ] );
  4         13  
72 4 50       25 $self->row_names( [ @{$row_names}[@$idcs] ] ) if (@$row_names);
  4         12  
73 4 50       12 $self->row_data( [ @{$row_data}[@$idcs] ] ) if (@$row_data);
  4         9  
74 4 50       10 $self->row_data_names( [ @{$row_data_names}[@$idcs] ] ) if (@$row_data_names);
  0         0  
75              
76 4         24 return $self;
77             }
78              
79             sub sort {
80 4     4 1 100 my $self = shift;
81 4         8 my $cb = shift;
82              
83 4         9 my $assay = $self->assay;
84 4         10 my $nrow = $self->nrow;
85 4         14 my @idcs = 0 .. ( $nrow - 1 );
86 4         18 @idcs = sort { $cb->( $assay->[$a], $assay->[$b], $a, $b ) } @idcs;
  8         30  
87              
88 4         36 return $self->_reorder( \@idcs, @_ );
89             }
90              
91             sub shuffle {
92 0     0 1 0 my $self = shift;
93              
94 0         0 my $nrow = $self->nrow;
95 0         0 my @idcs = 0 .. ( $nrow - 1 );
96 0         0 @idcs = List::Util::shuffle(@idcs);
97              
98 0         0 return $self->_reorder( \@idcs, @_ );
99             }
100              
101             sub _idx {
102 0     0   0 my ( $self, $m, $name ) = @_;
103 0 0       0 unless ($name) {
104 0         0 return -1;
105             }
106 0     0   0 firstidx { $_ eq $name } @{ $self->$m };
  0         0  
  0         0  
107             }
108              
109             sub row_idx {
110 0     0 1 0 my ( $self, $name ) = @_;
111 0 0       0 return -1 unless ($name);
112 0     0   0 return firstidx { $_ eq $name } @{ $self->row_names };
  0         0  
  0         0  
113             }
114              
115             sub col_idx {
116 15     15 1 1729 my ( $self, $name ) = @_;
117 15 50       35 return -1 unless ($name);
118 15     24   45 return firstidx { $_ eq $name } @{ $self->col_names };
  24         82  
  15         80  
119             }
120              
121             sub transpose {
122 1     1 1 31 my $self = shift;
123              
124 1     3   5 my @assay_t = MapCarU { [@_] } @{ $self->{assay} };
  3         51  
  1         8  
125 1     3   11 my @row_data_t = MapCarU { [@_] } @{ $self->{row_data} };
  3         41  
  1         3  
126 1     3   8 my @col_data_t = MapCarU { [@_] } @{ $self->{col_data} };
  3         48  
  1         3  
127              
128 1         52 return __PACKAGE__->new(
129             assay => \@assay_t,
130             na_value => $self->na_value,
131             row_names => Clone::clone( $self->col_names ),
132             row_data => \@row_data_t,
133             col_names => Clone::clone( $self->row_names ),
134             col_data => \@col_data_t,
135             col_data_names => Clone::clone( $self->row_data_names ),
136             row_data_names => Clone::clone( $self->col_data_names ),
137             );
138             }
139              
140 0     0 1 0 sub make_consistent { die 'function not implemented, yet'; }
141              
142             sub _idx_grep {
143 0     0   0 my ( $self, $names, $cb ) = ( shift, shift, shift );
144              
145 0 0   0   0 return [ indexes { $_ =~ $cb } @$names ] if ref $cb eq 'Regexp';
  0         0  
146 0     0   0 return [ indexes { $cb->($_) } @$names ];
  0         0  
147             }
148              
149             sub row_idx_grep {
150 0     0 0 0 my $self = shift;
151              
152 0         0 return $self->_idx_grep( $self->row_names, @_ );
153             }
154              
155             sub col_idx_grep {
156 0     0 0 0 my $self = shift;
157              
158 0         0 return $self->_idx_grep( $self->col_names, @_ );
159             }
160              
161             sub add_col {
162 1     1 1 36 my ( $self, $assay_col, $name, $col_data ) = @_;
163              
164 1         3 my @names;
165 1 50       6 push @names, $name if ( defined($name) );
166              
167 1 50 33     4 $col_data = [ map { [$_] } @$col_data ] if ( $col_data && @$col_data );
  0         0  
168              
169 1         2 my $data;
170 1 50       5 if ( ref $assay_col eq 'CODE' ) {
171 0         0 $data = $assay_col;
172             } else {
173 1         4 $data = [ map { [$_] } @$assay_col ];
  3         8  
174             }
175              
176 1         7 return $self->cbind( $data, \@names, $col_data );
177             }
178              
179             sub cbind {
180 1     1 1 3 my ( $self, $data, $names, $col_data_n ) = @_;
181 1         5 my $na_value = $self->na_value;
182              
183 1   50     7 $col_data_n //= [];
184 1 50 33     6 push @{ $self->col_names }, @$names if ( $names && @$names );
  1         5  
185              
186 1         3 my $assay = $self->assay;
187 1         4 my $ncol = $self->ncol;
188              
189 1 50       7 if ( ref $data eq 'CODE' ) {
    50          
190 0         0 for ( my $i = 0; $i < @$assay; $i++ ) {
191 0         0 local $_ = $assay->[$i];
192 0         0 push @{ $assay->[$i] }, $data->( $assay->[$i], $i );
  0         0  
193             }
194             } elsif ( ref $data eq 'ARRAY' ) {
195 1 50       4 die "number of rows differ" unless ( @$data == @$assay );
196 1         5 for ( my $i = 0; $i < @$assay; $i++ ) {
197 3         4 push @{ $assay->[$i] }, @{ $data->[$i] };
  3         6  
  3         9  
198             }
199             } else {
200 0         0 die "no code or array";
201             }
202              
203 1         5 my $col_data = $self->col_data;
204             # the assay is already updated, so ncol will represent the new number
205 1         3 my $ncol_added = $self->ncol - $ncol;
206 1 50 33     7 die "col data dims differ" if ( @$col_data && @$col_data_n && @$col_data != @$col_data_n );
      33        
207              
208 1 50       6 my $col_data_ncol = @$col_data > @$col_data_n ? @$col_data : @$col_data_n;
209 1         4 for ( my $i = 0; $i < $col_data_ncol; $i++ ) {
210 2   50     6 $col_data->[$i] //= [ (undef) x $ncol ];
211 2   50     4 push @{ $col_data->[$i] }, @{ $col_data_n->[$i] // [ (undef) x $ncol_added ] };
  2         3  
  2         12  
212             }
213              
214 1         5 return $self;
215             }
216              
217             sub add_cols {
218 0     0 1 0 die 'function not implemented, yet';
219             }
220              
221             sub group {
222 10     10 1 27 return shift->group_by_idcs(@_);
223             }
224              
225             sub ncol {
226 22     22 1 43 my $assay = shift->assay;
227 22 50 33     70 return unless ( $assay && @$assay );
228              
229 22         32 return scalar @{ $assay->[0] };
  22         68  
230             }
231              
232 4     4 1 5 sub nrow { scalar @{ shift->assay }; }
  4         11  
233              
234             sub as_hash {
235 0     0 1 0 my $self = shift;
236 0         0 my %data;
237 0         0 for my $entry (qw/assay col_data row_data row_names col_names row_data_names col_data_names meta_data/) {
238 0         0 $data{$entry} = $self->$entry;
239             }
240 0         0 return \%data;
241             }
242              
243             sub encode_as_json {
244 0     0 1 0 my $self = shift;
245 0         0 my $js = JSON::XS->new->utf8->allow_nonref->indent(1); #->canonical(1);
246 0         0 return $js->encode( $self->as_hash );
247             }
248              
249             sub json_spew {
250 0     0 1 0 my ( $self, $f ) = @_;
251 0 0       0 open my $fh, '>', $f or die "Can't open filehandle: $!";
252 0         0 print $fh $self->encode_as_json;
253 0         0 close $fh;
254             }
255              
256             sub subset {
257 4     4 1 2408 my ( $self, $cb ) = @_;
258              
259 4         13 my $assay = $self->assay;
260 4         10 my $row_names = $self->row_names;
261 4         6 my $row_data = $self->row_data;
262 4         9 my $row_data_names = $self->row_data_names;
263              
264 4         5 my $idcs;
265 4 100       17 if ( ref $cb eq 'CODE' ) {
    50          
266 3         16 for ( my $i = 0; $i < @$assay; $i++ ) {
267 9         41 local $_ = $assay->[$i];
268 9 100       18 push @$idcs, $i if ( $cb->( $assay->[$i], $i ) );
269             }
270             } elsif ( ref $cb eq 'ARRAY' ) {
271 1         2 $idcs = $cb;
272             }
273              
274 4         23 my @row_names_new;
275             my @assay_new;
276 4         0 my @row_data_new;
277              
278 4         10 for my $i (@$idcs) {
279 5         33 push @assay_new, Clone::clone( $assay->[$i] );
280 5 50 33     39 push @row_names_new, Clone::clone( $row_names->[$i] ) if ( $row_names && @$row_names );
281 5 50 33     42 push @row_data_new, Clone::clone( $row_data->[$i] ) if ( $row_data && @$row_data );
282             }
283              
284 4         165 return __PACKAGE__->new(
285             assay => \@assay_new,
286             na_value => $self->na_value,
287             row_names => \@row_names_new,
288             row_data => \@row_data_new,
289             row_data_names => Clone::clone( $self->row_data_names ),
290             col_names => Clone::clone( $self->col_names ),
291             col_data => Clone::clone( $self->col_data ),
292             col_data_names => Clone::clone( $self->col_data_names ),
293             );
294             }
295              
296             sub _invert_idcs {
297 5     5   26 my ( $n, $idcs ) = @_;
298              
299 5         9 my @inv;
300 5         12 my %m = map { $_ => 1 } @$idcs;
  6         20  
301 5         19 for ( my $i = 0; $i < $n; $i++ ) {
302 14 100       45 push @inv, $i unless ( $m{$i} );
303             }
304 5         14 return \@inv;
305             }
306              
307             #TODO names to idcs as wantarray?
308             sub merge {
309 6     6 1 4206 my ( $se_x, $se_y, $param ) = @_;
310              
311 6   50     14 my %param = ( join => 'inner', %{ $param // {} } );
  6         35  
312 6   66     24 my $by_x = $param{by_x} // $param{by};
313 6   66     16 my $by_y = $param{by_y} // $param{by};
314 6 100       29 die "join needs same amount of rows on both sets" unless ( @$by_x == @$by_y );
315 5         15 my $idcs_x = $se_x->col_names_to_idcs($by_x);
316 5         12 my $idcs_y = $se_y->col_names_to_idcs($by_y);
317              
318 5         12 my $col_data_x = $se_x->col_data;
319 5         18 my $col_data_y = $se_y->col_data;
320              
321 5 50 66     31 die "col data has different number of rows"
      66        
322             if ( @$col_data_x && @$col_data_y && @$col_data_x != @$col_data_y );
323              
324 5         12 my $assay_x = $se_x->assay;
325 5         11 my $assay_y = $se_y->assay;
326              
327 5         12 my $groups_x = $se_x->group($idcs_x);
328 5         12 my $groups_y = $se_y->group($idcs_y);
329              
330 5         8 my $ncol_common = @$by_x;
331 5         12 my $ncol_only_x = $se_x->ncol - $ncol_common;
332 5         11 my $ncol_only_y = $se_y->ncol - $ncol_common;
333 5         11 my $ncol_total = $ncol_only_x + $ncol_only_y + $ncol_common;
334              
335 5         13 my @row_names_new;
336             my @assay_new;
337 5         0 my @row_data_new;
338 5         42 my @keys = List::MoreUtils::uniq( keys(%$groups_x), keys(%$groups_y) );
339 5         18 my $inv_by_y = _invert_idcs( $se_y->ncol, $idcs_y );
340 5         12 for my $k (@keys) {
341 19         31 my $data_x = $groups_x->{$k};
342 19         31 my $data_y = $groups_y->{$k};
343              
344 19 100 100     77 next if ( $param{join} eq 'inner' && !( $data_x && $data_y ) );
      100        
345 16 100 66     45 if ( $param{join} eq 'left' || $param{join} eq 'full' ) {
346 8 100 66     30 next if ( $param{join} eq 'left' && !$data_x );
347             $data_y //= {
348             idcs => [-1],
349             rows => [ [ (undef) x $se_y->ncol ] ],
350             key => $data_x->{key},
351             key_names => $data_x->{key_names},
352 6   100     31 row_names => [],
353             row_data => []
354             };
355             }
356 14 100 66     50 if ( $param{join} eq 'right' || $param{join} eq 'full' ) {
357 4 100 66     16 next if ( $param{join} eq 'right' && !$data_y );
358 3         9 my @row = ( (undef) x $se_x->ncol );
359 3         4 @row[@$idcs_x] = @{ $data_y->{key} };
  3         65  
360             $data_x //= {
361             idcs => [-1],
362             rows => [ \@row ],
363             key => $data_y->{key},
364             key_names => $data_y->{key_names},
365 3   100     15 row_names => [],
366             row_data => []
367             };
368             }
369              
370 13         23 for ( my $i = 0; $i < @{ $data_x->{rows} }; $i++ ) {
  26         78  
371 13         22 for ( my $j = 0; $j < @{ $data_y->{rows} }; $j++ ) {
  26         58  
372              
373 13         19 my @row = ( @{ $data_x->{rows}[$i] }, @{ $data_y->{rows}[$j] }[@$inv_by_y] );
  13         26  
  13         45  
374 13         29 push @assay_new, \@row;
375             push @row_names_new, $data_x->{row_names}[$i]
376 13 100 66     30 if ( $data_x->{row_names} && @{ $data_x->{row_names} } );
  13         37  
377             push @row_data_new, Clone::clone( $data_x->{row_data}[$i] )
378 13 100 66     29 if ( $data_x->{row_data} && @{ $data_x->{row_data} } );
  13         92  
379             }
380             }
381             }
382              
383 5         13 my @col_names = ( @{ $se_x->col_names }, @{ $se_y->col_names }[@$inv_by_y] );
  5         14  
  5         16  
384 5         9 my @col_data;
385              
386 5 50 33     22 if ( @$col_data_x || @$col_data_y ) {
387 5         19 my $col_data_nrow = max( ( scalar @$col_data_x ), ( scalar @$col_data_y ) );
388 5         12 for ( my $i = 0; $i < $col_data_nrow; $i++ ) {
389 9   50     21 my $cd_x = $col_data_x->[$i] // [ (undef) x $ncol_only_x ];
390 9   100     19 my $cd_y = $col_data_y->[$i] // [ (undef) x $ncol_only_y ];
391 9         20 push @col_data, [ @$cd_x, @{$cd_y}[@$inv_by_y] ];
  9         30  
392             }
393             }
394 5         192 return __PACKAGE__->new(
395             assay => \@assay_new,
396              
397             na_value => $se_x->na_value,
398             row_data => \@row_data_new,
399             col_data => \@col_data,
400              
401             row_data_names => Clone::clone( $se_x->row_data_names ),
402             col_data_names => Clone::clone( $se_x->col_data_names ),
403              
404             row_names => \@row_names_new,
405             col_names => \@col_names,
406             );
407             }
408              
409             sub inconsistencies {
410 0     0 1 0 my $self = shift;
411              
412             # TODO
413             # check if assay is rectangular
414             # check if row data is rectangular and has the
415              
416             }
417              
418             sub _is_rectangular_matrix {
419 3     3   139 my $aoa = shift;
420              
421 3 50       11 return unless ( ref $aoa eq 'ARRAY' );
422 3         5 my $rlen;
423 3         11 for ( my $i = 0; $i < @$aoa; $i++ ) {
424 6         10 my $row = $aoa->[$i];
425 6 100 66     29 return unless ( $row && ref $row eq 'ARRAY' );
426 5 100       11 $rlen = @$row unless ( defined($rlen) );
427 5 100       18 return unless ( @$row == $rlen );
428             }
429 1         6 return 1;
430             }
431              
432             sub clone {
433 3     3 1 5864 my $self = shift;
434 3         257 return __PACKAGE__->new(
435             assay => Clone::clone( $self->assay ),
436             na_value => $self->na_value,
437             col_data => Clone::clone( $self->col_data ),
438             col_names => Clone::clone( $self->col_names ),
439             row_names => Clone::clone( $self->row_names ),
440             row_data => Clone::clone( $self->row_data ),
441             row_data_names => Clone::clone( $self->row_data_names ),
442             col_data_names => Clone::clone( $self->col_data_names ),
443             meta_data => Clone::clone( $self->meta_data ),
444             );
445             }
446              
447             sub rbind {
448 0     0 1 0 my $self = shift;
449 0         0 my $row_elems = shift;
450 0         0 my $names = shift;
451 0         0 my $row_data_elems = shift;
452              
453 0         0 my $col_names = $self->col_names;
454 0         0 my $row_data_names = $self->row_data_names;
455              
456 0         0 my @rows;
457 0         0 for my $o (@$row_elems) {
458 0 0       0 if ( ref $o eq 'ARRAY' ) {
459 0         0 push @rows, $o;
460             } else {
461 0         0 push @rows, [ @{$o}{@$col_names} ];
  0         0  
462             }
463             }
464 0         0 my @row_data;
465 0         0 for my $o (@$row_data_elems) {
466 0 0       0 if ( ref $o eq 'ARRAY' ) {
467 0         0 push @row_data, $o;
468             } else {
469 0         0 push @row_data, [ @{$o}{@$col_names} ];
  0         0  
470             }
471             }
472              
473 0         0 return $self->_rbind( \@rows, $names, \@row_data );
474             }
475              
476             sub _rbind {
477 0     0   0 my ( $self, $rows, $names, $row_data ) = @_;
478              
479 0         0 my $nrow = $self->nrow;
480             # FIXME check if all input params have the same length
481              
482 0         0 push @{ $self->assay }, @$rows;
  0         0  
483              
484 0 0 0     0 if ( $names && @$names ) {
485 0 0 0     0 $self->row_names->[ $nrow - 1 ] //= undef if ( $nrow > 0 );
486 0         0 push @{ $self->row_names }, @$names;
  0         0  
487             }
488              
489 0 0 0     0 if ( $row_data && @$row_data ) {
490 0 0 0     0 $self->row_data->[ $nrow - 1 ] //= [] if ( $nrow > 0 );
491 0         0 push @{ $self->row_data }, @$row_data;
  0         0  
492             }
493              
494 0         0 return $self;
495             }
496              
497             sub dim {
498 0     0 1 0 my $self = shift;
499 0         0 return ( $self->nrow, $self->ncol );
500             }
501              
502             sub _max_dim {
503 3     3   7 my $aoa = shift;
504              
505 3 50       9 return unless ( ref $aoa eq 'ARRAY' );
506 3         3 my $max_ncol;
507 3         12 for ( my $i = 0; $i < @$aoa; $i++ ) {
508 9         15 my $row = $aoa->[$i];
509 9 50 33     29 return unless ( $row && ref $row eq 'ARRAY' );
510 9 100 66     36 $max_ncol = @$row if ( !defined($max_ncol) || @$row > $max_ncol );
511             }
512 3 50       8 return unless ( defined $max_ncol );
513              
514 3         7 return [ scalar(@$aoa), $max_ncol ];
515             }
516              
517             sub fill_assay {
518 0     0 0 0 my $self = shift;
519              
520 0         0 _Fill_2d($self->assay);
521 0         0 return $self;
522             }
523              
524             sub _Fill_2d {
525 3     3   3001 my $data = shift;
526 3   100     13 my $dim = shift // [ 0, 0 ];
527 3         6 my $na_value = shift;
528              
529 3 50 33     20 return unless ( $data && ref $data eq 'ARRAY' );
530 3         8 my $dim_data = _max_dim($data);
531 3 50       7 return unless ($dim);
532 3         12 my $nrow = max( $dim_data->[0], $dim->[0] );
533 3         19 my $ncol = max( $dim_data->[1], $dim->[1] );
534              
535 3         9 for ( my $i = 0; $i < $nrow; $i++ ) {
536 11   100     27 $data->[$i] //= [];
537 11 100       16 next if ( @{ $data->[$i] } == $ncol );
  11         26  
538 9         20 for ( my $j = 0; $j < $ncol; $j++ ) {
539 42   100     120 $data->[$i][$j] //= $na_value;
540             }
541             }
542 3         9 return $data;
543             }
544              
545             sub _na_fill_1d {
546 0     0   0 my $data = shift;
547 0         0 my $dim = shift;
548 0   0     0 my $na_value = shift // $NA_VALUE;
549              
550 0 0 0     0 return unless ( $data && ref $data eq 'ARRAY' );
551 0         0 my $len = max( $dim, @$data );
552 0 0       0 return unless ($len);
553              
554 0         0 for ( my $i = 0; $i < $len; $i++ ) {
555 0   0     0 $data->[$i] //= $na_value;
556             }
557 0         0 return $data;
558             }
559              
560 0     0 1 0 sub add_rows { shift->rbind(@_) }
561              
562             sub aggregate {
563 0     0 1 0 return shift->aggregate_by_idcs(@_);
564             }
565              
566             sub aggregate_by_idcs {
567 0     0 1 0 my ( $self, $idcs, $cb, $col_names ) = @_;
568              
569 0         0 my $row_groups = $self->group($idcs);
570              
571 0         0 my @agg_assay;
572             my @agg_row_names;
573 0         0 my @agg_row_data;
574 0         0 my @agg_row_data_names;
575 0         0 for my $v ( values %$row_groups ) {
576 0         0 local $_ = $v;
577             my ( $row, $row_name, $row_data, $row_data_name )
578 0         0 = $cb->( $v->{key}, $v->{rows}, $v->{idcs} );
579 0 0       0 push @agg_assay, $row if ( defined($row) );
580 0 0       0 push @agg_row_names, $row_name if ( defined($row_name) );
581 0 0       0 push @agg_row_data, $row_data if ( defined($row_data) );
582 0 0       0 push @agg_row_data_names, $row_data_name if ( defined($row_data_name) );
583             }
584              
585 0   0     0 return __PACKAGE__->new(
586             assay => \@agg_assay,
587             na_value => $self->na_value,
588             col_names => $col_names // [],
589             row_names => \@agg_row_names,
590             row_data_names => \@agg_row_data_names,
591             row_data => \@agg_row_data,
592             );
593             }
594              
595             sub col_names_to_idcs {
596 11     11 1 16 my $self = shift;
597 11         22 my @names = @_;
598 11 50       71 return unless (@names);
599 11 50 33     53 @names = @{ $names[0] } if ( @names == 1 && ref $names[0] eq 'ARRAY' );
  11         24  
600              
601 11         22 my @idcs = map { $self->col_idx($_) } @names;
  14         35  
602 11 50   14   43 die "could not find all idcs " . jon( ", ", @names ) if ( any { $_ < 0 } @idcs );
  14         33  
603 11         32 return \@idcs;
604             }
605              
606             sub aggregate_by_names {
607 0     0 1 0 my ( $self, $names, $cb, $col_names ) = @_;
608 0         0 my $idcs = $self->col_names_to_idcs($names);
609              
610 0         0 return $self->aggregate_by_idcs( $idcs, $cb, $col_names );
611             }
612              
613             sub group_by_idcs {
614 10     10 1 18 my ( $self, $idcs, $args ) = @_;
615              
616 10         20 my $assay = $self->assay;
617 10         18 my $row_names = $self->row_names;
618 10         17 my $row_data = $self->row_data;
619 10         20 my $row_data_names = $self->row_data_names;
620 10         13 my %groups;
621 10         17 my @key_names = @{ $self->col_names }[@$idcs];
  10         26  
622 10         26 for ( my $i = 0; $i < @$assay; $i++ ) {
623 29         44 my @key = @{ $assay->[$i] }[@$idcs];
  29         62  
624 29         60 my $key = join( $;, @key );
625 29   50     212 $groups{$key} //= {
626             idcs => [],
627             rows => [],
628             key => \@key,
629             key_names => \@key_names,
630             row_names => [],
631             row_data => [],
632             row_data_names => $row_data_names
633             };
634 29         45 push @{ $groups{$key}{idcs} }, $i;
  29         59  
635 29         44 push @{ $groups{$key}{rows} }, $assay->[$i];
  29         63  
636 29 100 66     91 push @{ $groups{$key}{row_names} }, $row_names->[$i] if ( $row_names && @$row_names );
  24         46  
637 29 100 66     88 push @{ $groups{$key}{row_data} }, $row_data->[$i] if ( $row_data && @$row_data );
  24         65  
638             }
639 10         36 return \%groups;
640             }
641              
642             #if ( $uniq && !defined($vidx) ) {
643             #$map{$k} = 1;
644             #} elsif ( not defined $vidx ) {
645             #$map{$k}++;
646             #} elsif ($uniq) {
647             #confess "strict mode: two times the same key $k" if ( $is_strict && defined( $map{$k} ) );
648             #$map{$k} = ( ref $vidx ? [ @{$r}[@$vidx] ] : ( $vidx eq 'all' ? $r : $r->[$vidx] ) );
649             #} else {
650             #$map{$k} //= [];
651             #push @{ $map{$k} }, ( ref $vidx ? [ @{$r}[@$vidx] ] : ( $vidx eq 'all' ? $r : $r->[$vidx] ) );
652             #}
653             #}
654             #}
655             #return \%map;
656             #}
657              
658             sub group_by_names {
659 0     0 1 0 my $self = shift;
660 0         0 my $names = shift;
661              
662 0         0 my $idcs = $self->col_names_to_idcs($names);
663              
664 0         0 return $self->group( $idcs, @_ );
665             }
666              
667             sub names_to_idcs {
668 0     0 1 0 return shift->col_names_to_idcs(@_);
669             }
670              
671             sub c2i {
672 0     0 0 0 my $i = 0;
673 0         0 my %I = ( map { $_ => $i++ } @{ shift->col_names } );
  0         0  
  0         0  
674              
675 0 0       0 return unless (%I);
676 0 0       0 return wantarray ? %I : \%I;
677             }
678              
679 0     0 1 0 sub col_idx_map { shift->c2i }
680              
681             sub row_idx_map {
682 0     0 1 0 my $i = 0;
683 0         0 my %I = ( map { $_ => $i++ } @{ shift->row_names } );
  0         0  
  0         0  
684              
685 0 0       0 return unless (%I);
686 0 0       0 return wantarray ? %I : \%I;
687             }
688              
689             sub col_rename {
690 0     0 1 0 my ( $self, $old, $new ) = @_;
691              
692 0         0 my $idx = $self->col_idx($old);
693 0 0       0 die if ( $idx < 0 );
694 0         0 $self->col_names->[$idx] = $new;
695 0         0 return $self;
696             }
697              
698             sub row_apply {
699 0     0 1 0 my ( $self, $cb ) = @_;
700              
701 0         0 my @res;
702 0         0 my $assay = $self->assay;
703 0         0 for ( my $i = 0; $i < @$assay; $i++ ) {
704 0         0 local $_ = $assay->[$i];
705 0         0 push @res, $cb->( $assay->[$i], $i );
706             }
707 0         0 return \@res;
708             }
709              
710             sub element_apply {
711 1     1 0 41 my ( $self, $cb ) = @_;
712              
713 1         2 my @res;
714 1         3 my $assay = $self->assay;
715 1         5 for ( my $i = 0; $i < @$assay; $i++ ) {
716 3         7 my $j = 0;
717 3   50     5 my @row_res = map { $cb->( $_, $i, $j++ ) } @{ $assay->[$i] // [] };
  9         70  
  3         9  
718              
719 3         23 push @res, \@row_res;
720             }
721 1         3 return \@res;
722             }
723              
724             sub col_apply {
725 0     0 1 0 my ( $self, $cb ) = @_;
726              
727 0         0 my @res;
728 0     0   0 my @assay_t = MapCarU { [@_] } @{ $self->{assay} };
  0         0  
  0         0  
729              
730 0         0 for ( my $i = 0; $i < @assay_t; $i++ ) {
731 0         0 local $_ = $assay_t[$i];
732 0         0 push @res, $cb->( $assay_t[$i], $i );
733             }
734 0         0 return \@res;
735             }
736              
737             sub apply {
738 0     0 1 0 my ( $self, $dir, $cb, @args ) = @_;
739              
740 0 0 0     0 if ( $dir eq 'r' || $dir == 1 ) {
    0 0        
    0 0        
      0        
741 0         0 return $self->row_apply( $cb, @args );
742             } elsif ( $dir eq 'c' || $dir == 2 ) {
743 0         0 return $self->col_apply( $cb, @args );
744             } elsif ( $dir eq 'rc' || $dir eq 'cr' || $dir == 3 ) {
745 0         0 return $self->element_apply( $cb, @args );
746             }
747             }
748              
749             sub slice_by_idcs {
750 1     1 1 4 my ( $self, $idcs ) = @_;
751              
752 1         48 my @assay_new = map { [ @{$_}[@$idcs] ] } @{ $self->assay };
  3         7  
  3         10  
  1         9  
753 1         3 my @new_colnames;
754 1 50       5 @new_colnames = @{ $self->col_names }[@$idcs] if ( $self->has_col_names );
  1         5  
755              
756 1         2 my @new_coldata;
757 1 50       4 @new_coldata = map { [ @{$_}[@$idcs] ] } @{ $self->col_data } if ( $self->has_col_data );
  2         4  
  2         6  
  1         4  
758              
759 1         65 return __PACKAGE__->new(
760             assay => \@assay_new,
761             na_value => $self->na_value,
762             row_names => Clone::clone( $self->row_names ),
763             row_data => Clone::clone( $self->row_data ),
764             col_names => \@new_colnames,
765             col_data => \@new_coldata,
766             row_data_names => Clone::clone( $self->row_data_names ),
767             col_data_names => Clone::clone( $self->col_data_names ),
768             );
769             }
770              
771             sub has_col_names {
772 1     1 1 4 my $c = shift->col_names;
773 1   33     9 return $c && @$c;
774             }
775              
776             sub has_col_data {
777 1     1 1 4 my $c = shift->col_data;
778 1   33     8 return $c && @$c;
779             }
780              
781             sub has_row_data {
782 0     0 1 0 my $c = shift->row_data;
783 0   0     0 return $c && @$c;
784             }
785              
786             sub has_row_names {
787 0     0 1 0 my $c = shift->row_names;
788 0   0     0 return $c && @$c;
789             }
790              
791             sub extract_col_by_idx {
792 0     0 1 0 my ( $self, $idx ) = @_;
793              
794 0         0 my $assay = $self->assay;
795 0         0 return [ map { $_->[$idx] } @$assay ];
  0         0  
796             }
797              
798             sub extract_col_by_name {
799 0     0 1 0 my $self = shift;
800 0         0 return $self->extract_col_by_idx( $self->col_idx(@_) );
801             }
802              
803             sub slice_by_names {
804 1     1 1 1730 my ( $self, $names ) = @_;
805 1         4 my $idcs = $self->col_names_to_idcs($names);
806 1         18 return $self->slice_by_idcs($idcs);
807             }
808              
809             sub each {
810 0     0 1   my $self = shift;
811 0           $self->apply( 1, @_ );
812 0           return $self;
813             }
814              
815             sub uniq {
816 0     0 1   my $self = shift;
817              
818 1     1   6945 no warnings 'uninitialized';
  1         2  
  1         120  
819 0           my %seen;
820             return $self->subset(
821             sub {
822 0 0   0     return if ( $seen{ join $;, @$_ }++ );
823 0           return 1;
824             }
825 0           );
826             }
827              
828             # sub grep -> subset
829             # from Mojo::Collection
830             # first
831             # last
832              
833             1;
834              
835             __END__
836              
837             =head1 NAME
838              
839             Bio::Gonzales::SummarizedExperiment - represent experimental matrix-like data (assay) with features and sample info
840              
841             =head1 SYNOPSIS
842              
843              
844             =head1 DESCRIPTION
845              
846             L<http://bioconductor.org/packages/devel/bioc/vignettes/SummarizedExperiment/inst/doc/SummarizedExperiment.html>
847              
848             =head1 ATTRIBUTES
849              
850             =head2 assay
851              
852             my $assay = $se->assay;
853              
854             Return the assay of the summarized experiment.
855              
856             =head2 col_data
857              
858             my $col_data = $se->col_data;
859             $se->col_data(\@col_data);
860              
861             =head2 row_data
862              
863             =head2 row_names
864              
865             =head2 col_names
866              
867             =head2 row_data_names
868              
869             =head2 col_data_names
870              
871             =head2 meta_data
872              
873             =head2 na_value
874              
875             Set the NA value if the object is stored. Internally, while I<in memory>, undef will be
876             used. So this value will only have an effect if data is read or written somewhere.
877             Default is C<Bio::Gonzales::SummarizedExperiment::NA_VALUE>.
878              
879             =head1 METHODS
880              
881             =head2 data
882              
883             my $assay = $se->data;
884              
885             A alias for assay.
886              
887             =head2 add_col
888              
889             =head2 add_cols
890              
891             =head2 add_rows
892              
893             =head2 aggregate
894              
895             =head2 C<< $se = $se->aggregate_by_idcs(\@idcs, sub { ... }, \@col_names)
896              
897             The callback gets passed the grouping keys, rows and row indices. C<$_> is set to the
898             group has that comes from the (internally used) C<< $se->group >> function.
899              
900             sub {
901             my ($key, $rows, $row_idcs) = @_;
902             my $group = $_;
903             }
904              
905             =head2 C<< $se = $se->aggregate_by_names(\@names, sub { ... }, \@col_names)
906              
907             =head2 apply
908              
909             =head2 as_hash
910              
911             =head2 cbind
912              
913             =head2 clone
914              
915             =head2 col_apply
916              
917             =head2 col_idx
918              
919             =head2 col_idx_map
920              
921             my $I = $se->col_idx_map;
922             my %I = $se->col_idx_map;
923              
924             Returns a hash that maps the column names to their column index. col_idx_map is context
925             sensitve and returns a hash in list context and a hash reference in scalar context.
926              
927             =head2 col_idx_match
928              
929             =head2 col_names_to_idcs
930              
931             =head2 col_rename
932              
933              
934             =head2 dim
935              
936             =head2 each
937              
938             =head2 extract_col_by_idx
939              
940             =head2 extract_col_by_name
941              
942             =head2 group
943              
944             =head2 group_by_idcs
945              
946             =head2 group_by_names
947              
948             =head2 has_col_data
949              
950             =head2 has_col_names
951             =head2 has_row_data
952             =head2 has_row_names
953             =head2 header
954             =head2 header_idx
955             =head2 header_idx_match
956             =head2 inconsistencies
957             =head2 json_spew
958             =head2 make_consistent
959             =head2 merge
960              
961             Merge two SummarizedExperiment objects.
962              
963             use Bio::Gonzales::SummarizedExperiment;
964             my $se_x = Bio::Gonzales::SummarizedExperiment->new(
965             assay => [ [ 1, "homer", "simpson" ], [ 2, "bart", "simpson" ], [ 3, "lisa simpson" ] ],
966             col_names => [qw(user_id first_name surname)]
967             );
968            
969             my $se_y = Bio::Gonzales::SummarizedExperiment->new(
970             assay => [ [ 1, 120 ], [ 2, 20 ] ],
971             col_names => [qw(user_id weight_kg)]
972             );
973            
974             # inner join by default
975             my $merged_se = $se_x->merge($se_y, { by => [ 'user_id' ] });
976            
977             # user_id first_name surname weight_kg
978             # 1 homer simpson 120
979             # 2 bart simpson 20
980             # Lisa is missing, because the $se_y lacks weight information.
981              
982             =head2 names_to_idcs
983             =head2 ncol
984             =head2 nrow
985             =head2 rbind
986             =head2 row_apply
987              
988             Apply a callback to each row. C<$se->row_apply(@args)> is equivalent to C<$se->apply(1, @args)>
989              
990              
991             use Bio::Gonzales::SummarizedExperiment;
992             my $se = Bio::Gonzales::SummarizedExperiment->new(
993             assay => [ [ 1, "homer", "simpson" ], [ 2, "bart", "simpson" ], [ 3, "lisa", "simpson" ] ],
994             col_names => [qw(user_id first_name surname)]
995             );
996              
997             # WITHOUT MODIFYING THE SUMMARIZEDEXPERIMENT OBJECT
998             $se->row_apply(sub { (my $name = $_->[1]) =~ s/[ra]/z/; $name });
999             # [ 'homez', 'bzrt', 'lisz' ];
1000              
1001             $se->extract_col_by_idx(1);
1002             # [ 'homer', 'bart', 'lisa' ];
1003              
1004              
1005             # WITH MODIFYING THE SUMMARIZEDEXPERIMENT OBJECT
1006             $se->row_apply(sub { $_->[1] =~ s/[ra]/z/; $_->[1] });
1007             # [ 'homez', 'bzrt', 'lisz' ];
1008              
1009             $se->extract_col_by_idx(1);
1010             # [ 'homez', 'bzrt', 'lisz' ];
1011              
1012              
1013              
1014             =head2 row_idx
1015             =head2 row_idx_map
1016             =head2 row_idx_match
1017             =head2 shuffle
1018              
1019             =head2 slice_by_idcs
1020              
1021             $se->slice_by_idcs(\@idcs);
1022             $se->slice_by_idcs([0,5,13]);
1023              
1024             Extract a column-"slice" from the summarized experiment. The indices select the columns.
1025              
1026             =head2 slice_by_names
1027              
1028             =head2 slurp_assay
1029            
1030             my $se = Bio::Gonzales::SummarizedExperiment->slurp_assay($source, \%params);
1031             my $se = Bio::Gonzales::SummarizedExperiment->slurp_assay("data.csv", { header => 1, sep => ';' });
1032              
1033             Create a new summarized experiment from matrix/tabular data.
1034              
1035             =head2 sort
1036             =head2 spew_assay
1037             =head2 subset
1038             =head2 encode_as_json
1039             =head2 transpose
1040             =head2 uniq
1041              
1042             =head1 LIMITATIONS
1043              
1044             =head1 NOTES
1045              
1046             By convention,
1047              
1048             =over 4
1049              
1050             =item * constructor or function arguments ending in C<?> are optional
1051              
1052             =item * methods ending in C<!> will modify the object it is called on
1053              
1054             =back
1055              
1056             =head1 SEE ALSO
1057              
1058             =head1 AUTHOR
1059              
1060             jw bargsten, C<< <jwb at cpan dot org> >>
1061              
1062             =cut
1063              
1064