【问题标题】:Perl: Compare keys of 2 hashes & print the difference between the closest keysPerl:比较 2 个哈希的键并打印最接近的键之间的差异
【发布时间】:2012-12-28 05:32:03
【问题描述】:

更新(2013 年 16 月 1 日)

鲍罗丁指出了另一种我完全忽略的可能性。
实际 文件中(我手动坐下来开始查看 46 个文件,每个文件大约 10MB 大),在某些情况下,对于 File1 中的特定值,没有 File2 中存在较小的 值(但存在 较大的 值)。

同样,对于 File1 中的特定值,File2 中不存在 更大 值(但 更小 值)

我在此处更新示例文件和所需的输出以反映此更新。

更新(2013 年 15 月 1 日)

我已经更新了所需的输出,以解决 File1 中的值匹配 File2 中的值的情况。感谢 Borodin 指出这种情况。


我有 2 个文件,如下所示:

文件1

 chr1   10227  
 chr1   447989  
 chr1   535362
 chr1   856788
chr1    249240496

文件2

chr1    11017
chr1    11068
chr1    23525
chr1    439583
chr1    454089
chr1    460017
chr1    544711
chr1    546239
chr1    856788
chr1    249213429
chr1    249214499
chr1    249239072

我需要做的是 file1 中的 foreach 值,例如。 10227,从 file2 中找到最接近的 两个 值。其中一个值会更大,而另一个值会更小。 所以在file1中取10227,在file2中最接近的值是925011017。现在需要计算差异,即 9250 - 10227 = -97711017 - 10227 = 790 以提供如下输出(制表符分隔):

期望的输出

chr1   10227   No   790   No Match
chr1   447989  No   6100  -8406
chr1   535362  No   9349  -75345
chr1   856788  Yes  
chr1   249240496 No No Match -25997

我认为最快的方法是使用哈希来读取 2 个文件,将数字作为 keys 并将 1 作为值。

到目前为止,我编写的代码给出了10227file2 中所有值的差异。与447989535682 类似。 如何阻止这种情况并仅找到最接近的数字之间的差异,一个是 >10227,另一个是 10227

代码

use 5.014;
use warnings;

#code to enter lsdpeak and pg4 data into hash with KEYS as the numerical values, VALUE as 1

#Assign filename
my $file1 = 'lsdpeakmid.txt';
my $file2 = 'pg4mid.txt';

#Open file
open my $fh1, '<', $file1 or die $!;
open my $fh2, '<', $file2 or die $!;

#Read in file linewise
my %hash1;
while(<$fh1>){

    my $key1 = (split)[1];
    $hash1{$key1} = 1;

}


    my %hash2;
    while(<$fh2>){
        my $key2 = (split)[1];

    }


foreach my $key1 (sort keys %hash1){

    foreach my $key2 (sort keys %hash2){

    say $key2-$key1;

    }

}

#Exit
exit;

感谢您抽出宝贵时间解决问题。我将不胜感激任何评论/回答。

【问题讨论】:

  • 两个文件中的数字是升序排列的吗?
  • @melpomene 是的。它们不是连续的,但肯定是按升序排列的
  • 如果file2 中有一个值与file1 中的一个匹配 怎么办?
  • @Borodin 好吧,这不太可能,但是有可能。在这种情况下,应该打印一个 0 表示值匹配。
  • 我只有一台平板电脑,没有 PC 就无法轻松编写修改后的解决方案。当我周末回家时,我会尽量记住回到这个问题。如果所有边界都小于或大于任何一个值,您仍然需要考虑该怎么做 - 即没有包含该值的区间。

标签: perl hash bioinformatics


【解决方案1】:

一种方式:

#!/usr/bin/perl
use strict;
use warnings;
use List::Util qw(first);

open my $fh1,'<','file1' or die $!;
open my $fh2,'<','file2' or die $!;
my %h1;

while(<$fh2>){
        chomp;
        my ($k,$v)=split(/\s+/);
        push @{$h1{$k}}, $v;
}
close $fh2;

while (<$fh1>){
        chomp;
        my ($k, $v)=split(/\s+/);
        my $bef=first{$_ >= $v}@{$h1{$k}};
        $bef=defined $bef?$bef-$v:"No match";
        my $aft=first{$_ <= $v}reverse @{$h1{$k}};
        $aft=defined $aft?$aft-$v:"No match";
        my $str=sprintf("%-8s %-10d %-5s %-8s %-8s",$k, $v,$bef?"No":"Yes",$bef?$bef:"",$aft?$aft:"");
        print $str, "\n";
}
close $fh1;

第一个while 循环读取第二个文件并创建一个哈希,其中键为 chr1,值是包含 chr1 的所有值的数组引用。

foreach 块按数字顺序对所有键进行排序。 第二个while循环处理file1的记录,使用List::Utilfirst函数得到结果。

first函数被使用了两次:一次,获取当前值的第一个最大值,第二次:获取当前值的最后一个最小值,这是通过在@987654330上使用first获得的@sorted 数组。

第一个功能: 第一个函数返回数组中第一个满足条件的数。

first{$_ &gt; $v}@{$h1{$k}} => 这将获取数组中大于当前数字的第一个数字。说为 10227,先返回 11017。

接下来需要的是 10227 之前的最后一个最小数字。为了得到这个,第一个函数应用于反向数组。

first{$_ &lt; $v}reverse @{$h1{$k}} => 这将返回小于 10227 的第一个数字,并且由于数组反转,我们得到的实际上是 10227 之前的最后一个最小数字,即 9250。

运行时:

chr1     10227      No    790      No match
chr1     447989     No    6100     -8406
chr1     535362     No    9349     -75345
chr1     856788     Yes
chr1     249240496  No    No match -1424

【讨论】:

  • 大师您好!感谢您的回答。我只需要再澄清一点。您能否逐步解释一下,这里如何使用 second first 函数:(first{$_ &lt; $v}reverse @{$h1{$k}})-$v
  • 非常感谢您的澄清。 reverse 的想法太棒了!你认为这段代码可以稍微更新一下,以包含 $_ = $v 的任何偶然事件吗?例如。这行得通吗:if (first{$_ eq $v}@{$h1{$k}}) {say "0" }
  • 您的意思是指第二个文件中的数字相等的情况?如果是这样,如果数字匹配,您能否在原始帖子中发布预期结果?
  • 1.如果 $bef 不包含任何内容,则分配“不匹配”,否则将其减去 $v。 2. sprintf 行用于格式化打印语句
  • 对于?,请参见Perldoc%-8s 中的条件运算符部分,表示将其对齐到宽度8 的左侧。
【解决方案2】:

首先,我们读入第二个文件并将值放入一个数组。我进一步假设这个chr1 是恒定的并且可以安全地丢弃:

#!/usr/bin/perl
use strict; use warnings;
my @file2;
open my $fh2, "<", "file2" or die $!;
while (<$fh2>) {
  my (undef, $num) = split;
  die "the number contains illegal characters" if $num =~ /\D/;
  push @file2, $num;
}
@file2 = sort {$a <=> $b} @file2; # sort ascending
# remove previous line if sorting is already guaranteed.

然后,我们定义一个 sub 来查找数组中的两个值。它只是在排序列表中找到某个值的基本算法的一种变体(在 O(log n) 中),并且应该比迭代每个值执行得更好,至少在大集合上是这样。此外,它不需要为每个值反转整个列表。

sub find {
  my ($num, $arrayref) = @_;

  # exit if array is too small
  return unless @$arrayref >= 2;
  # exit if $num is outside the values of this array (-1 is last element)
  return if $num <= $arrayref->[0] or $arrayref->[-1] < $num;

   my ($lo, $hi) = (1, $#$arrayref);
  my $i = int(($lo+$hi)/2); # start in the middle

  # iterate until
  #   a) the previous index contains a number that is smaller than $num and
  #   b) the current index contains a number that is greater or equal to $num.
  until($arrayref->[$i-1] < $num and $num <= $arrayref->[$i]) {
    # make $i the next lower or upper bound.
    # instead of going into an infinite loop (which would happen if we
    # assign $i to a variable that already holds the same value), we discard
    # the value and move on towards the middle.
          # $i is too small
    if    ($num >  $arrayref->[$i]  ) { $lo = ($lo == $i ? $i+1 : $i) }
          # $i is too large
    elsif ($num <= $arrayref->[$i-1]) { $hi = ($hi == $i ? $i-1 : $i) }
          # in case I made an error:
    else                              { die "illegal state" }
    # calculate the next index
    $i  = int(($lo+$hi)/2);
  }
  return @{$arrayref}[$i-1, $i];
}

剩下的就很简单了:

open my $fh1, "<", "file1" or die $!;
while (<$fh1>) {
  my ($chr, $num) = split;
  die "the number contains illegal characters" if $num =~ /\D/;
  if (my ($lo, $hi) = find($num, \@file2)) {
    if ($hi == $num) {
      print join("\t", $chr, $num, "Yes"), "\n";
    } else {
      print join("\t", $chr, $num, "No", $hi-$num, $lo-$num), "\n";
    }
  } else {
    # no matching numbers were found in file 2
    print join("\t", $chr, $num, "No-match"), "\n";
  }
}

输出:

chr1    10227   No      790     -977                                                            
chr1    447989  No      6100    -8406                                                           
chr1    535362  No      9349    -75345                                                          
chr1    856788  Yes

【讨论】:

  • 先生您好!感谢您的回答。您能否逐步详细说明子程序?从昨晚开始,我一直在尝试理解它(因此直到现在还没有 cmets),虽然我能够弄清楚 -&gt; 的含义,甚至使它与备用 $$ 一起工作,并且还可以看到实现是binary search 算法,确切的流程顺序让我无法理解..
  • @Neal 我改进了算法;它现在涵盖了更多的情况,并且不会再陷入无限循环。另外,我添加了一些 cmets 应该更容易理解。 -&gt;[$index] 只是用于从数组引用中取消引用值。输出代码现在可以区分找到的匹配项和丢失的匹配项。
  • 示例:08 的数组。我们想找到5初始化: $lo=1$hi=8$i=int(4.5)=4第一次迭代:$i-1 处的值为3$i 处的值为4$i 太小了,所以现在$lo=4。新的$i6下一次迭代:$i-1 的值是5$i 的值是6$i 太大,所以现在$hi=6。新的$i5下一次迭代:$i-1 处的值为4$i 处的值为5。终止条件匹配。返回两个值。
  • 非常感谢。我又经历了一遍。与此同时,鲍罗丁指出了另一种我完全忽略的可能性。我已经相应地更新了问题。
【解决方案3】:

在这里散列不是一个好的选择,因为从file2 中找到正确边界的唯一方法是搜索值列表,而散列并不能帮助做到这一点。

此程序的工作原理是将file2 的所有边界放入数组@boundaries,然后在该数组中搜索从file1 读取的每个值,以找到第一个更大的边界值。那么这个边界和之前的边界就是必需的,并且在print 语句中完成了算术运算。

请注意,如果file2 包含匹配边界,或者没有大于或不小于给定值的边界,则此代码将出现问题。

use strict;
use warnings;

use Data::Dump;

my $file1 = 'lsdpeakmid.txt';
my $file2 = 'pg4mid.txt';

my @boundaries = do {
  open my $fh, '<', $file2 or die $!;
  map { (split)[1] } <$fh>;
};

open my $fh, '<', $file1 or die $!;

while (my $line = <$fh>) {
  chomp $line;
  my @vals = split ' ', $line;
  my $val = $vals[-1];
  for my $i (1 .. $#boundaries) {
    if ($boundaries[$i] > $val) {
      print join(' ', @vals, $boundaries[$i] - $val, $boundaries[$i-1] - $val), "\n";
      last;
    }
  }
}

输出

chr1 10227 790 -977
chr1 447989 6100 -8406
chr1 535362 9349 -75345

【讨论】:

  • 先生您好!我要求进一步澄清。 1 为什么必须同时使用mapsplit 才能将元素放入@boundaries。一个简单的split 不工作吗?
  • 2.LINE 是做什么的?
  • 3. 在这行代码$boundaries[$i] - $val, $boundaries[$i-1] - $val 中如何确保取第一个最大边界元素和第一个最低边界元素?
  • LINE 是一个标签,它允许我在内部for 循环中写入next LINE。没有它,next 将开始 for 的下一次迭代,而不是读取文件的下一行。并且因为您可以保证对file2 进行排序,所以如果我从头开始搜索大于$val 的第一个值,那么显然前一个值小于$val。正如我在答案中指出的那样,这只有在没有任何边界与值匹配并且总是至少有一个较小的边界和一个较大的边界时才有效。
  • 我已经重构了我的代码以避免需要标签,因为它应该首先编写。
猜你喜欢
  • 2011-10-27
  • 1970-01-01
  • 1970-01-01
  • 2012-06-01
  • 1970-01-01
  • 2014-02-05
  • 1970-01-01
  • 1970-01-01
  • 2012-03-13
相关资源
最近更新 更多