source: branches/stable/PERL_SCRIPTS/SPECIES/clone_taxonomy.pl

Last change on this file was 18470, checked in by westram, 4 years ago
  • also check inconsistencies on top-level.
  • ensure listed inconsistencies contain no duplicates.
  • Property svn:executable set to *
File size: 11.7 KB
Line 
1#!/usr/bin/perl
2# ========================================================= #
3#                                                           #
4#   File      : clone_taxonomy.pl                           #
5#   Purpose   : clone taxonomy entries                      #
6#                                                           #
7#   Coded by Ralf Westram (coder@reallysoft.de) in Jul 20   #
8#   http://www.arb-home.de/                                 #
9#                                                           #
10# ========================================================= #
11
12use strict;
13use warnings;
14
15# ---------------------------------
16#      you may configure here:
17
18my $tax_field      = 'tax_ltp';
19my $fullname_field = 'fullname_ltp';
20
21my $show_correct_entries = 0; # list genera with correct and unique tax-entries? (previously was default behavior)
22my $check_invalid_inconsistencies = 1; # set to 0 to hide invalid entries from inconsistency check
23
24# ---------------------------------
25
26BEGIN {
27  if (not exists $ENV{'ARBHOME'}) { die "Environment variable \$ARBHOME has to be defined"; }
28  my $arbhome = $ENV{'ARBHOME'};
29  push @INC, "$arbhome/lib";
30  push @INC, "$arbhome/PERL_SCRIPTS/lib";
31  1;
32}
33
34use ARB;
35use tools;
36
37sub is_unclassified($) {
38  # tax entry will be overwritten, when this return 1
39  my ($tax_value) = @_;
40  if ($tax_value) {
41    if (($tax_value =~ /unclassified/io) and not ($tax_value =~ /;/o)) { 1; }
42    elsif ($tax_value eq '') { 1; }
43    else { 0; }
44  }
45  else { 1; }
46}
47sub is_valid_tax4genus($$) {
48  my ($tax,$genus) = @_;
49
50  if (not ($tax =~ /;$genus$/)) { 0; }
51  elsif ($tax =~ /^(.*;)*\s*(unclassified|undefined)\s*(;.*)*$/io) { 0; } # comment line to accept ";Unclassified;" inside tax
52  else { 1; }
53}
54
55my %tax_seen  = (); # key=tax (full or prefix); value=occur count
56my %tax_level = (); # key=tax (full or prefix); value=level (=number of ;)
57my %tax_invalid = (); # key=tax (full only); value=1 -> invalid entry (not 6 tax levels)
58
59sub register_tax($$);
60sub register_tax($$) {
61  my ($tax,$level) = @_; # level==0 -> initial call
62
63  if (exists $tax_seen{$tax}) {
64    $tax_seen{$tax}++;
65  }
66  else {
67    $tax_seen{$tax} = 1;
68    my $semi = $tax;
69    $semi =~ s/[^;]//go;
70    $tax_level{$tax} = length($semi);
71    if ($level==0) {
72      if ($tax_level{$tax} != 5) { # 5 semicolons == 6 tax levels
73        $tax_invalid{$tax} = 1;
74        $tax = ''; # do not register prefixes
75      }
76    }
77  }
78  if ($tax =~ /;[^;]+$/o) {
79    register_tax($`,$level+1);
80  }
81}
82
83sub log_tax_inconsistencies() {
84  # log tax entries with an invalid number of levels:
85  my @sorted_invalid = sort keys %tax_invalid;
86  foreach my $tax (@sorted_invalid) {
87    my $lev = $tax_level{$tax}+1;
88    print "Warning: '$tax' has $lev hierarchy levels\n";
89  }
90
91  # search inconsistencies in tax hierarchy
92  my @valid_taxa = map {
93    if ($check_invalid_inconsistencies) { $_; }
94    elsif (exists $tax_invalid{$_}) { ; }
95    else { $_; }
96  } keys %tax_seen;
97
98  my %tax_ending_in = (); # key = suffix of tax (last part); value = array containing tax
99  foreach my $tax (@valid_taxa) {
100    my $suffix;
101    if ($tax =~ /;([^;]+)$/o) {
102      $suffix = $1;
103    }
104    else {
105      $suffix = $tax;
106    }
107    my $end_r = $tax_ending_in{$suffix};
108    if (defined $end_r) {
109      push @$end_r, $tax;
110    }
111    else {
112      $tax_ending_in{$suffix} = [ $tax ];
113    }
114  }
115
116  my @sorted_suffixes = sort keys %tax_ending_in;
117  foreach my $suf (@sorted_suffixes) {
118    my $end_r = $tax_ending_in{$suf};
119    my %unique_taxa = map { $_ => 1; } @$end_r;
120    my @unique_taxa = keys %unique_taxa;
121    my $count = scalar(@unique_taxa);
122    $count>0 || die;
123    if ($count>1) {
124      print "Warning: taxonomy part '$suf' used multiple times:\n";
125      foreach my $tax (sort @$end_r) {
126        print "- '$tax'\n";
127      }
128    }
129  }
130}
131
132sub dump_tax_statistic() {
133  my @sorted_taxa = sort keys %tax_seen;
134  my %level_count = (); # key=level; value=sum of 'tax_seen' of taxa on 'level'
135  my $max_level = 0;
136
137  foreach my $tax (@sorted_taxa) {
138    my $seen = $tax_seen{$tax};
139    my $level = $tax_level{$tax};
140
141    if (not exists $level_count{$level}) {
142      $level_count{$level} = $seen;
143    }
144    else {
145      $level_count{$level} += $seen;
146    }
147    if ($level>$max_level) {
148      $max_level = $level;
149    }
150  }
151
152  for (my $level=0; $level<=($max_level+1); $level++) {
153    my $lcount = $level_count{$level};
154    if ((not defined $lcount) or (not $lcount)) {
155      print "Found no taxonomy entries with level $level\n";
156    }
157    else {
158      print "Taxonomy entries with level $level:\n";
159      foreach my $tax (@sorted_taxa) {
160        if ($tax_level{$tax}==$level) {
161          my $pcount = $lcount;
162          if ($tax =~ /;[^;]+$/o) {
163            $pcount = $tax_seen{$`};
164          }
165          my $seen = $tax_seen{$tax};
166          my $pc = $seen * 100.0 / $pcount;
167
168          print sprintf("%5i=%5.1f%%: %s\n", $seen, $pc, $tax);
169        }
170      }
171    }
172  }
173}
174
175my %match_tax = (); # key=genus, value=matching tax entry
176my %diff_tax  = (); # key=genus, value=matching, but different tax entry
177my %bad_tax   = (); # key=genus, value=non-matching tax entry
178my %seen_target = (); # key=genus, value=1 -> seen target entry (undefined tax + marked(if requested))
179my %mod_targets = (); # key=genus, value=count of modified targets
180
181sub init_genus($) {
182  my ($genus) = @_;
183  if (not defined $seen_target{$genus}) {
184    $match_tax{$genus} = undef;
185    $diff_tax{$genus} = undef;
186    $bad_tax{$genus} = undef;
187    $seen_target{$genus} = 0;
188    $mod_targets{$genus} = 0;
189  }
190}
191
192my $reg_genus = qr/^([^\s]+)\s/o;
193
194sub clone_tax($$) {
195  my ($markedOnly,$detailed_statistic) = @_;
196
197  my $gb_main = ARB::open(":","r");
198  $gb_main || expectError('db connect (no running ARB?)');
199
200  dieOnError(ARB::begin_transaction($gb_main), 'begin_transaction');
201
202  my $no_fullname = 0;
203  my $has_fullname = 0;
204  my $genus_detected = 0;
205
206  # scan tax and fullnames:
207  for (my $gb_species = BIO::first_species($gb_main);
208       $gb_species;
209       $gb_species = BIO::next_species($gb_species)) {
210
211    my $fullname = BIO::read_string($gb_species, $fullname_field);
212    if ($fullname) {
213      $has_fullname++;
214      if ($fullname =~ $reg_genus) {
215        my $genus = $1;
216        init_genus($genus);
217
218        $genus_detected++;
219
220        my $tax = BIO::read_string($gb_species, $tax_field);
221        if ($tax) {
222          $tax =~ s/[;\s]*$//ig; # silently ignore ';' and spaces at end of tax-entry. Also will clone w/o these.
223        }
224        my $unclassified = is_unclassified($tax);
225        if ($unclassified) {
226          my $is_target = $markedOnly ? ARB::read_flag($gb_species) : 1;
227          if ($is_target) { $seen_target{$genus} = 1; }
228        }
229        else {
230          if (is_valid_tax4genus($tax,$genus)) {
231            if (defined $match_tax{$genus}) {
232              if ($match_tax{$genus} ne $tax) {
233                if (not defined $diff_tax{$genus}) {      # only store first different tax
234                  $diff_tax{$genus} = $tax;
235                }
236              }
237            }
238            else {
239              $match_tax{$genus} = $tax;
240            }
241          }
242          else {
243            if (not defined $bad_tax{$genus}) {
244              $bad_tax{$genus} = $tax;
245            }
246          }
247
248          if ($markedOnly==0 || ARB::read_flag($gb_species)==1) {
249            register_tax($tax,0);
250          }
251        }
252      }
253      else {
254        my $name = BIO::read_string($gb_species, "name");
255        print "Warning: no genus detected in $fullname_field '$fullname' (species=$name)\n";
256      }
257    }
258    else {
259      $no_fullname++;
260    }
261  }
262
263  print "species w/o  $fullname_field: $no_fullname\n";
264  print "species with $fullname_field: $has_fullname\n";
265  print "species with genus detected: $genus_detected\n";
266
267  my @genera = sort keys %seen_target;
268
269  print "different genera detected: ".scalar(@genera)."\n";
270
271  for (my $pass=1; $pass<=4; ++$pass) {
272    foreach my $genus (@genera) {
273      if (defined $bad_tax{$genus}) {
274        if ($pass==1) {
275          print "Warning: seen invalid $tax_field content for '$genus': ".$bad_tax{$genus}."\n";
276        }
277      }
278      elsif (defined $diff_tax{$genus}) {
279        if ($pass==2) {
280          print "Warning: $tax_field entries differ for '$genus':\n";
281          print "  '".$match_tax{$genus}."'\n";
282          print "  '".$diff_tax{$genus}."'\n";
283        }
284      }
285      elsif (not defined $match_tax{$genus}) {
286        if ($pass==3) {
287          print "Warning: No valid $tax_field entry seen for '$genus'\n";
288        }
289      }
290      elsif ($seen_target{$genus}==0) {
291        if (($pass==4) and ($show_correct_entries!=0)) {
292          print "All".($markedOnly ? " marked " : " ")."entries for '$genus' contain correct $tax_field content: ".$match_tax{$genus}."\n";
293        }
294      }
295    }
296  }
297
298  for (my $gb_species = BIO::first_species($gb_main);
299       $gb_species;
300       $gb_species = BIO::next_species($gb_species)) {
301
302    my $fullname = BIO::read_string($gb_species, $fullname_field);
303    if ($fullname) {
304      $has_fullname++;
305      if ($fullname =~ $reg_genus) {
306        my $genus = $1;
307        if ((defined $match_tax{$genus}) and not (defined $diff_tax{$genus} or defined $bad_tax{$genus})) {
308          if ($seen_target{$genus}==1) {
309            my $tax = BIO::read_string($gb_species, $tax_field);
310            if ($tax) {
311              $tax =~ s/[;\s]*$//ig; # silently ignore ';' and spaces at end of tax-entry. Also will clone w/o these.
312            }
313            my $unclassified = is_unclassified($tax);
314            if ($unclassified) {
315              my $is_target = $markedOnly ? ARB::read_flag($gb_species) : 1;
316              if ($is_target) {
317                my $error = BIO::write_string($gb_species, $tax_field, $match_tax{$genus});
318                if ($error) { die $error; }
319                $mod_targets{$genus}++;
320              }
321            }
322          }
323        }
324      }
325    }
326  }
327
328  my $mod_sum = 0;
329  my $mod_genera = 0;
330  foreach my $genus (@genera) {
331    my $mod = $mod_targets{$genus};
332    if ($mod > 0) {
333      print "Changed $mod entries of '$genus' into: ".$match_tax{$genus}."\n";
334      $mod_sum += $mod;
335      $mod_genera++;
336    }
337  }
338
339  log_tax_inconsistencies();
340
341  print "Overall: changed $mod_sum entries for $mod_genera genera.\n";
342
343  if ($detailed_statistic==1) { dump_tax_statistic(); }
344
345  ARB::commit_transaction($gb_main);
346  ARB::close($gb_main);
347}
348
349sub main() {
350  my $args = scalar(@ARGV);
351  if ($args==0) {
352    print "Usage: clone_taxonomy.pl [--marked|--all] [--statistic]\n";
353    print "\n";
354    print "Tries to clone the field '$tax_field' from existing entries.\n";
355    print "\n";
356    print "Operates on the database currently running in ARB\n";
357    print "(call this script via \"Tools/Start XTERM\").\n";
358    print "\n";
359    print "Uses the first word of field '$fullname_field' to create\n";
360    print "separate clusters (for separate genera). Each cluster is expected\n";
361    print "to use only ONE common value in field '$tax_field'.\n";
362    print "That value has to consist of multiple words separated by ';'. The last\n";
363    print "word has to match the first word from field '$fullname_field'.\n";
364    print "Missing entries (or value \"unclassified\" or similar) are accepted and will\n";
365    print "be replaced by the detected common value for each genus.\n";
366    print "\n";
367    print "If --statistic is specified, additional statistic about taxonomy is dumped.\n";
368    print "\n";
369    print "This script logs many warnings about inconsistencies in '$tax_field'.\n";
370  }
371  else {
372    my $arg = shift @ARGV;
373
374    my $markedOnly         = 0;
375    my $detailed_statistic = 0;
376
377    while (defined $arg) {
378      if ($arg eq '--marked') { $markedOnly = 1; }
379      elsif ($arg eq '--all') { $markedOnly = 0; }
380      elsif ($arg eq '--statistic') { $detailed_statistic = 1; }
381      else { die "unknown argument '$arg'"; }
382      $arg = shift @ARGV;
383    }
384    clone_tax($markedOnly,$detailed_statistic);
385  }
386}
387
388main();
Note: See TracBrowser for help on using the repository browser.