File Coverage

lib/Pheno/Ranker.pm
Criterion Covered Total %
statement 118 129 91.4
branch 25 54 46.3
condition 0 3 0.0
subroutine 20 20 100.0
pod 0 3 0.0
total 163 209 77.9


line stmt bran cond sub pod time code
1             package Pheno::Ranker;
2              
3 3     3   160451 use strict;
  3         27  
  3         89  
4 3     3   15 use warnings;
  3         5  
  3         72  
5 3     3   1033 use autodie;
  3         30842  
  3         12  
6 3     3   19386 use feature qw(say);
  3         5  
  3         389  
7 3     3   2097 use Data::Dumper;
  3         20784  
  3         203  
8 3     3   23 use File::Basename qw(dirname);
  3         7  
  3         255  
9 3     3   23 use Cwd qw(abs_path);
  3         5  
  3         134  
10 3     3   507 use File::Spec::Functions qw(catdir catfile);
  3         898  
  3         156  
11 3     3   1779 use Moo;
  3         22039  
  3         26  
12 3     3   7153 use Types::Standard qw(Str Int Num Enum ArrayRef HashRef Undef Bool);
  3         362107  
  3         33  
13 3     3   12775 use File::ShareDir::ProjectDistDir qw(dist_dir);
  3         110545  
  3         27  
14 3     3   1250 use List::Util qw(all);
  3         9  
  3         331  
15 3     3   1397 use Pheno::Ranker::IO;
  3         7  
  3         290  
16 3     3   1638 use Pheno::Ranker::Align;
  3         9  
  3         242  
17 3     3   22 use Pheno::Ranker::Stats;
  3         5  
  3         195  
18              
19 3     3   21 use Exporter 'import';
  3         4  
  3         216  
20             our @EXPORT_OK = qw($VERSION write_json);
21              
22             # Global variables:
23             $Data::Dumper::Sortkeys = 1;
24             our $VERSION = '0.00_4';
25             our $share_dir = dist_dir('Pheno-Ranker');
26 3     3   15 use constant DEVEL_MODE => 0;
  3         4  
  3         6910  
27              
28             # Misc variables
29             my (
30             $config_sort_by, $config_max_out, $config_max_number_var,
31             $config_seed, @config_allowed_terms
32             );
33             my $default_config_file = catfile( $share_dir, 'conf', 'config.yaml' );
34              
35             ############################################
36             # Start declaring attributes for the class #
37             ############################################
38              
39             # Complex defaults here
40             has 'config_file' => (
41             default => $default_config_file,
42             coerce => sub {
43             $_[0] // $default_config_file;
44             },
45             is => 'ro',
46             isa => sub { die "$_[0] is not a valid file" unless -e $_[0] },
47             trigger => sub {
48             my ( $self, $config_file ) = @_;
49             my $config = read_yaml($config_file);
50              
51             #####################
52             # Set config params #
53             #####################
54              
55             $config_sort_by = $config->{sort_by} // 'hamming';
56             $config_max_out = $config->{max_out} // 50;
57             $config_max_number_var = $config->{max_number_var} // 10_000;
58              
59             # Validate $config->{allowed_terms}
60             unless ( exists $config->{allowed_terms}
61             && ArrayRef->check( $config->{allowed_terms} )
62             && @{ $config->{allowed_terms} } )
63             {
64             die
65             "No <allowed terms> provided or not an array ref at\n$config_file\n";
66             }
67             @config_allowed_terms = @{ $config->{allowed_terms} };
68              
69             ###############################
70             # Set config exclusive params #
71             ###############################
72             $config_seed =
73             ( defined $config->{seed} && Int->check( $config->{seed} ) )
74             ? $config->{seed}
75             : 123456789;
76              
77             # Set on $self
78             $self->{primary_key} = $config->{primary_key} // 'id'; # setter;
79             $self->{exclude_properties_regex} = $config->{exclude_properties_regex}
80             // ''; # setter
81             $self->{array_terms} = $config->{array_terms} // ['foo']; # setter - To validate
82             $self->{array_regex} = $config->{array_regex} // '^(\w+):(\d+)'; # setter - To validate
83             $self->{format} = $config->{format}; # setter
84              
85             # Validate $config->{id_correspondence} if we have "real" array_terms
86             if ( $self->{array_terms}[0] ne 'foo' ) {
87             unless ( exists $config->{id_correspondence}
88             && HashRef->check( $config->{id_correspondence} ) )
89             {
90             die
91             "No <id_correspondence> provided or not a hash ref at\n$config_file\n";
92             }
93             $self->{id_correspondence} = $config->{id_correspondence}; # setter
94              
95             # Validate format if exists and check that has a match in config->{id_correspondence}
96             if ( exists $config->{format} && Str->check( $config->{format} ) ) {
97             die
98             "<$config->{format}> does not match any key from <id_correspondence>\n"
99             unless
100             exists $config->{id_correspondence}{ $config->{format} };
101             }
102             }
103             }
104             );
105              
106             has sort_by => (
107             default => $config_sort_by,
108             is => 'ro',
109             coerce => sub { $_[0] // $config_sort_by },
110             lazy => 1,
111             isa => Enum [qw(hamming jaccard)]
112             );
113              
114             has max_out => (
115             default => $config_max_out, # Limit to speed up runtime
116             is => 'ro',
117             coerce => sub { $_[0] // $config_max_out },
118             lazy => 1,
119             isa => Int
120             );
121              
122             has max_number_var => (
123             default => $config_max_number_var,
124             is => 'ro',
125             coerce => sub { $_[0] // $config_max_number_var },
126             lazy => 1,
127             isa => Int
128             );
129              
130             has hpo_file => (
131             default => catfile( $share_dir, 'db', 'hp.json' ),
132             coerce => sub {
133             $_[0] // catfile( $share_dir, 'db', 'hp.json' );
134             },
135             is => 'ro',
136             isa => sub { die "$_[0] is not a valid file" unless -e $_[0] },
137             );
138              
139             has poi_out_dir => (
140             default => catdir('./'),
141             coerce => sub {
142             $_[0] // catdir('./');
143             },
144             is => 'ro',
145             isa => sub { die "$_[0] dir does not exist" unless -d $_[0] },
146             );
147              
148             has [qw /include_terms exclude_terms/] => (
149             is => 'ro',
150             lazy => 1,
151              
152             #isa => ArrayRef [Enum $config->{allowed_terms}], # It's created at compile time and we don't have $config->{allowed_terms}
153             isa => sub {
154             my $value = shift;
155             die "<--include_terms> and <--exclude_terms> must be an array ref"
156             unless ref $value eq 'ARRAY';
157             die
158             qq/Invalid term in <--include_terms> or <--exclude_terms>. Allowed values are:\n/,
159             ( join ',', @config_allowed_terms ), "\n"
160             unless all {
161             my $term = $_;
162             grep { $_ eq $term } @config_allowed_terms
163             }
164             @$value;
165             },
166             default => sub { [] },
167             );
168              
169             has 'cli' => (
170             is => 'ro',
171             isa => Bool,
172             default => 0, # Set the default value to 0
173             coerce => sub { $_[0] // 0 }, # Coerce to 0 if undefined
174             );
175              
176             # Miscellanea atributes here
177             has [
178             qw/target_file weights_file out_file include_hpo_ascendants align align_basename export export_basename log verbose age/
179             ] => ( is => 'ro' );
180              
181             has [qw /append_prefixes reference_files patients_of_interest/] =>
182             ( default => sub { [] }, is => 'ro' );
183              
184             ##########################################
185             # End declaring attributes for the class #
186             ##########################################
187              
188             sub BUILD {
189              
190             # BUILD: is an instance method that is called after the object has been constructed but before it is returned to the caller.
191             # BUILDARGS is a class method that is responsible for processing the arguments passed to the constructor (new) and returning a hash reference of attributes that will be used to initialize the object.
192 8     8 0 267 my $self = shift;
193              
194             # ************************
195             # Start Miscellanea checks
196             # ************************
197              
198             # APPEND_PREFIXES
199             # Check that we have the right numbers of array elements
200 8 50       16 if ( @{ $self->{append_prefixes} } ) {
  8         33  
201              
202             # die if used without $self->{append_prefixes}
203             die "<--append_prefixes> needs at least 2 cohort files!\n"
204 0 0       0 unless @{ $self->{reference_files} } > 1;
  0         0  
205              
206             # die if #cohorts and #append-prefixes don't match
207             die "Numbers of items in <--r> and <--append-prefixes> don't match!\n"
208 0 0       0 unless @{ $self->{reference_files} } == @{ $self->{append_prefixes} };
  0         0  
  0         0  
209             }
210              
211             # PATIENTS-OF-INTEREST
212 8 50       18 if ( @{ $self->{patients_of_interest} } ) {
  8         108  
213              
214             # die if used without $self->{append_prefixes}
215             die "<--patients-of-interest> needs to be used with <--r>\n"
216 0 0       0 unless @{ $self->{reference_files} };
  0         0  
217             }
218              
219             # **********************
220             # End Miscellanea checks
221             # **********************
222             }
223              
224             sub run {
225              
226 8     8 0 219 my $self = shift;
227              
228             #print Dumper $self and die;
229              
230             # Load variables
231 8         19 my $reference_files = $self->{reference_files};
232 8         19 my $target_file = $self->{target_file};
233 8         17 my $weights_file = $self->{weights_file};
234 8         18 my $export = $self->{export};
235 8         15 my $export_basename = $self->{export_basename};
236 8         19 my $include_hpo_ascendants = $self->{include_hpo_ascendants};
237 8         13 my $hpo_file = $self->{hpo_file};
238 8         13 my $align = $self->{align};
239 8         16 my $align_basename = $self->{align_basename};
240 8         14 my $out_file = $self->{out_file};
241 8         15 my $cohort_files = $self->{cohort_files};
242 8         13 my $append_prefixes = $self->{append_prefixes};
243 8         18 my $max_out = $self->{max_out};
244 8         15 my $sort_by = $self->{sort_by};
245 8         18 my $primary_key = $self->{primary_key};
246 8         15 my $poi = $self->{patients_of_interest};
247 8         16 my $poi_out_dir = $self->{poi_out_dir};
248 8         15 my $cli = $self->{cli};
249              
250             # die if --align dir does not exist
251 8 100       191 my $align_dir = defined $align ? dirname($align) : '.';
252 8 50       115 die "Directory <$align_dir> does not exist (used with --align)\n"
253             unless -d $align_dir;
254              
255 8 50       39 my $export_dir = defined $export ? dirname($export) : '.';
256 8 50       80 die "Directory <$export_dir> does not exist (used with --export)\n"
257             unless -d $export_dir;
258              
259             # We assing weights if <--w>
260             # NB: The user can exclude variables by using variable: 0
261 8         56 my $weight = validate_json($weights_file);
262              
263             # Now we load $hpo_nodes, $hpo_edges if --include_hpo_ascendants
264             # NB: we load them within $self to minimize the #args
265 6         14 my $nodes = my $edges = undef;
266 6 50       18 ( $nodes, $edges ) = parse_hpo_json( read_json($hpo_file) )
267             if $include_hpo_ascendants;
268 6         20 $self->{nodes} = $nodes; # setter
269 6         14 $self->{edges} = $edges; # setter
270              
271             ###############################
272             # START READING -r | -cohorts #
273             ###############################
274              
275             # *** IMPORTANT ***
276             # We have three modes of operation:
277             # 1 - intra-cohort (--r) a.json
278             # 2 - inter-cohort (--r) a.json b.json c.json
279             # 3 - patient (assigned automatically if -t)
280              
281             # *** IMPORTANT ***
282             # $ref_data is an array array where each element is the content of the file (e.g, [] or {})
283 6         17 my $ref_data = [];
284 6         11 for my $cohort_file ( @{$reference_files} ) {
  6         22  
285 6 50       103 die "$cohort_file does not exist\n" unless -f $cohort_file;
286              
287             # Load JSON file as Perl data structure
288 6         69 my $json_data = io_yaml_or_json(
289             {
290             filepath => $cohort_file,
291             mode => 'read'
292             }
293             );
294              
295             # Check for existence of primary_key otherwise die
296 6         41 my $msg =
297             "Sorry, <$cohort_file> does not contain primary_key <$primary_key> term and it's mandatory\n";
298 6 50       33 if ( ref $json_data eq ref [] ) { # array
299 6 50       27 die $msg unless exists $json_data->[0]->{$primary_key};
300             }
301             else { # hash
302 0 0       0 die $msg unless exists $json_data->{$primary_key};
303             }
304              
305             # Load data into array
306 6         26 push @$ref_data, $json_data;
307             }
308              
309             # In <inter-cohort> we join --cohorts into one but we rename the values of primary_key
310             # NB: Re-using $ref_data to save memory
311 6         47 $ref_data = append_and_rename_primary_key(
312             {
313             ref_data => $ref_data,
314             append_prefixes => $append_prefixes,
315             primary_key => $primary_key
316             }
317             );
318              
319             ##############################
320             # ENDT READING -r | -cohorts #
321             ##############################
322              
323             #-------------------------------
324             # Write json for $poi if --poi |
325             #-------------------------------
326             # *** IMPORTANT ***
327             # It will exit when done (dry-run)
328             write_poi(
329             {
330             ref_data => $ref_data,
331             poi => $poi,
332             poi_out_dir => $poi_out_dir,
333             primary_key => $primary_key,
334             verbose => $self->{verbose}
335             }
336             )
337 6 50 0     33 and exit
338             if @$poi;
339              
340             # We will process $ref_data to get stats on coverage
341 6         29 my $coverage_stats = coverage_stats($ref_data);
342              
343             # We have to check if we have BFF|PXF or others unless defined at config
344             add_attribute( $self, 'format', check_format($ref_data) )
345 6 100       36 unless defined $self->{format}; # setter via sub
346              
347             # First we create:
348             # - $glob_hash => hash with all the COHORT keys possible
349             # - $ref_hash => BIG hash with all individiduals' keys "flattened"
350 6         41 my ( $glob_hash, $ref_hash ) =
351             create_glob_and_ref_hashes( $ref_data, $weight, $self );
352              
353             # Limit the number of variables if > $self-{max_number_var}
354             # *** IMPORTANT ***
355             # Change only performed in $glob_hash
356             $glob_hash = randomize_variables( $glob_hash, $self )
357 6 50       39 if keys %$glob_hash > $self->{max_number_var};
358              
359             # Second we peform one-hot encoding for each individual
360 6         29 my $ref_binary_hash = create_binary_digit_string( $glob_hash, $ref_hash );
361              
362             # Hases to be serialized to JSON if <--export>
363 6         53 my $hash2serialize = {
364             glob_hash => $glob_hash,
365             ref_hash => $ref_hash,
366             ref_binary_hash => $ref_binary_hash,
367             coverage_stats => $coverage_stats
368             };
369              
370             # Perform cohort comparison
371 6 100       58 cohort_comparison( $ref_binary_hash, $self ) unless $target_file;
372              
373             # Perform patient-to-cohort comparison and rank if (-t)
374 6 100       26 if ($target_file) {
375              
376             ####################
377             # START READING -t #
378             ####################
379              
380             # local $tar_data is for patient
381 1         13 my $tar_data = array2object(
382             io_yaml_or_json( { filepath => $target_file, mode => 'read' } ) );
383              
384             ##################
385             # END READING -t #
386             ##################
387              
388             # The target file has to have $_->{$primary_key} otherwise die
389             die
390             "Sorry, <$target_file> does not contain primary_key <$primary_key> term and it's mandatory\n"
391 1 50       7 unless exists $tar_data->{$primary_key};
392              
393             # We store {primary_key} as a variable as it might be deleted from $tar_data (--exclude-terms id)
394 1         4 my $tar_data_id = $tar_data->{$primary_key};
395              
396             # Now we load the rest of the hashes
397 1         10 my $tar_hash = {
398             $tar_data_id => remap_hash(
399             {
400             hash => $tar_data,
401             weight => $weight,
402             self => $self
403             }
404             )
405             };
406              
407             # *** IMPORTANT ***
408             # The target binary is created from matches to $glob_hash
409             # Thus, it does not include variables ONLY present in TARGET
410 1         9 my $tar_binary_hash =
411             create_binary_digit_string( $glob_hash, $tar_hash );
412             my (
413 1         11 $results_rank, $results_align, $alignment_ascii,
414             $alignment_dataframe, $alignment_csv
415             )
416             = compare_and_rank(
417             {
418             glob_hash => $glob_hash,
419             ref_binary_hash => $ref_binary_hash,
420             tar_binary_hash => $tar_binary_hash,
421             weight => $weight,
422             self => $self
423             }
424             );
425              
426             # Print Ranked results to STDOUT only if CLI
427 1 50       7 say join "\n", @$results_rank if $cli;
428              
429             # Write txt (
430 1         12 write_array2txt( { filepath => $out_file, data => $results_rank } );
431              
432             # Write TXT for alignment (ALWAYS!!)
433 1 50       17 write_alignment(
    50          
434             {
435             align => $align ? $align : $align_basename, # DON'T -- $align // $align_basename,
436             ascii => $alignment_ascii,
437             dataframe => $alignment_dataframe,
438             csv => $alignment_csv
439             }
440             ) if defined $align;
441              
442             # Load keys into hash if <--e>
443 1 50       581 if ( defined $export ) {
444 0         0 $hash2serialize->{tar_hash} = $tar_hash;
445 0         0 $hash2serialize->{tar_binary_hash} = $tar_binary_hash;
446 0 0       0 $hash2serialize->{alignment_hash} = $results_align
447             if defined $align;
448             }
449             }
450              
451             # Dump to JSON if <--export>
452             # NB: Must work for -r and -t
453             serialize_hashes(
454             {
455 6 0       22 data => $hash2serialize,
    50          
456             export_basename => $export ? $export : $export_basename
457             }
458             ) if defined $export;
459              
460             # Return
461 6         5863 return 1;
462             }
463              
464             sub add_attribute {
465              
466             # Bypassing the encapsulation provided by Moo
467 3     3 0 10 my ( $self, $name, $value ) = @_;
468 3         9 $self->{$name} = $value;
469 3         7 return 1;
470             }
471              
472             1;
473              
474             =head1 NAME
475              
476             Convert::Pheno - A module that performs semantic similarity in PXF/BFF data structures and beyond (JSON|YAML)
477            
478             =head1 SYNOPSIS
479              
480             use Pheno::Ranker;
481              
482             # Create object
483             my $ranker = Pheno::Ranker->new(
484             {
485             reference_files => ['individuals.json'],
486             out_file => 'matrix.txt'
487             }
488             );
489              
490             # Run it (output are text files)
491             $ranker->run;
492              
493             =head1 DESCRIPTION
494              
495             We recommend using the included L<command-line interface|https://metacpan.org/dist/Pheno-Ranker/view/bin/pheno-ranker>.
496              
497             For a better description, please read the following documentation:
498              
499             =over
500              
501             =item General:
502              
503             L<https://cnag-biomedical-informatics.github.io/pheno-ranker>
504              
505             =item Command-Line Interface:
506              
507             L<https://github.com/CNAG-Biomedical-Informatics/pheno-ranker#readme>
508              
509             =back
510              
511             =head1 CITATION
512              
513             The author requests that any published work that utilizes C<Convert-Pheno> includes a cite to the the following reference:
514              
515             Rueda, M. et al. "Advancing Semantic Similarity Analysis of Phenotypic Data Stored in GA4GH Standards and Beyond. (2023) I<Manuscript in preparation>.
516              
517             =head1 AUTHOR
518              
519             Written by Manuel Rueda, PhD. Info about CNAG can be found at L<https://www.cnag.eu>.
520              
521             =head1 METHODS
522              
523             See L<https://cnag-biomedical-informatics.github.io/pheno-ranker/use-as-a-module>.
524              
525             =head1 COPYRIGHT
526              
527             This PERL file is copyrighted. See the LICENSE file included in this distribution.
528              
529             =cut
530