基于连续行的公共值覆盖行

时间:2013-08-19 14:39:39

标签: r perl awk

我有一个输入:

A  200-400  213  253  295  350  0011
A  200-400  260  295  315  000
A  200-400  205  263  295  111
B  800-900  801  832  840  843  870  890  895  00110101
B  800-900  801  823  850  010
B  800-900  850  1
.
.
.

0和最后一列中的1个值对应于从第三列到最后一列的值

我想生成一个与制表符分隔的矩阵,如下所示:

A 200-400  NA   213  253  NA   NA   295  NA   350 
A 200-400  NA   NA   NA   260  NA   295  315  NA
A 200-400  205  NA   NA   NA   263  295  NA   NA 
B 800-900  801  NA   832  840  843  NA   870  890 895 900
B 800-900  801  823  NA   NA   NA   850  NA   NA  NA  NA
B 800-900  NA   NA   NA   NA   NA   850  NA   NA  NA  NA

最后,将0和1值替换为相应的值和

A  200-400  NA  0    0    NA   NA   1    NA    1 
A  200-400  NA  NA   NA   0    NA   0    0     NA
A  200-400  1   NA   NA   NA   1    1    NA    NA 
B  800-900  0   NA   0    1    1    NA   0     1   0   1
B  800-900  0   1    NA   NA   NA   0    NA    NA  NA  NA
B  800-900  NA  NA   NA   NA   NA   1    NA    NA  NA  NA

非常感谢您的帮助。

1 个答案:

答案 0 :(得分:2)

多么有趣的问题。我将在Perl中回答。

我们需要同时读入相同范围的所有行。这些范围内的每个数字还必须记住它们来自哪条线。然后,我们可以对每个范围的数量进行排序,并重新组合这些线。

对于第一个范围,我们会有一组值,如

[213 => 1], [253 => 1], [295 => 1], [350 => 1],
[260 => 2], [295 => 2], [315 => 2],
[205 => 3], [263 => 3], [295 => 3],

我们应该删除常用数字,以便我们得到

[213 => 1], [253 => 1], [295 => 1, 2, 3], [350 => 1],
[260 => 2], [315 => 2],
[205 => 3], [263 => 3],

(顺序并不重要)。

我们可以通过第一个字段对这些项目进行排序:

my @sorted = sort { $a->[0] <=> $b->[0] } @items;

对于每一行,我们可以遍历已排序的项目,并根据行号决定是否打印NA或数字:

for my $line (1 .. 3) {
  my @fields = map { decide_if_number_or_na($line, @$_) } @sorted;
  ...
}

sub decide_if_number_or_na {
  my ($line, $number, @lines) = @_;
  return $number if grep { $line == $_ } @lines;  # ... if any of the lines is our line
  return "NA";
}

当然,我们应该立即发出正确的01值。

将所有这些结合在一起有点复杂。在解析输入时,我们需要将每一行与当前01模式相关联,记住前两个字段,并为项目构建数据结构。

结果代码遵循上述考虑因素,但需要一些快捷方式:订购后,每个数字的实际值对我们的项目并不重要,我们可以将其丢弃。

use strict; use warnings; use feature 'say';

my @lines;   # an array of hashes, which hold information about each line
my %ranges;  # a hash mapping range identifiers to number-to-occuring-line-array hashes

while (<>) {
  chomp;
  my ($letter, $range, @nums) = split;  # split everything into field ...
  my @pattern = split //, pop @nums;    # but the last field is a pattern, which we split into chars.
  push @{ $ranges{$range}{$_} }, $. for @nums;  # $. is the line no
  push @lines, {
    letter  => $letter,
    range   => $range,
    pattern => \@pattern,
    line    => $.,
  };
}

# simplify and sort the ranges:
for my $key (keys %ranges) {
  my $nums2lines = $ranges{$key};  # get the number-to-occuring-lines-array hashes
  # read the next statement bottom to top:
  my @items =
    map { $nums2lines->{$_} }  # 3. get the line number arrayref only (forget actual number, now that they are ordered)
    sort { $a <=> $b }         # 2. sort them numerically
    keys %$nums2lines;         # 1. get all numbers
  $ranges{$key} = \@items; # Remember these items at the prior position
}

# Iterate through all lines
for my $line (@lines) {
  # Unpack some variables
  my @pattern = @{ $line->{pattern} };
  my $lineno  = $line->{line};
  my $items   = $ranges{$line->{range}};

  # For each item, emit the next part of the pattern, or NA.
  my @fields  = map { pattern_or_na($lineno, @$_) ? shift @pattern : "NA" } @$items;
  say join "\t", $line->{letter}, $line->{range}, @fields;
}

sub pattern_or_na {
  my ($line, @lines) = @_;  # the second value (the specific number)
  return scalar grep { $_ == $line } @lines;  # returns true if a number is on this line
}

产生所需的输出。

这是非常复杂的代码,特别是对于初学者。它使用Perl引用和 autovivifiction 。此外,我使用了许多列表转换,例如sortmapgrep。此解决方案没有考虑具有相同范围的行是连续的,因此我不必将所有内容保留在内存中。这个解决方案更简单(原文如此!),但使用的内存比必要的多。

我建议您阅读perlreftutperlreperldsc联机帮助页,以便了解所有这些内容。