【问题标题】:Do I have a rounding error? Perl我有舍入错误吗? Perl
【发布时间】:2012-12-21 15:57:25
【问题描述】:

我的脚本应该执行以下操作。它需要一个旧的标量列表,并创建一个新的、相应的数字列表。旧列表称为@oldMarkers,新列表称为@newMarkers。

示例输入如下:chr1, chr2, IMP, chr3, IMP, IMP, IMP, chr4

示例输出如下:1, 2, 2.1, 3, 3.1, 3.2, 3.3, 4

脚本的重点是读取@oldMarkers 列表并输出一个列表,其中对于包含字母“chr”的元素的每个实例,一个整数被推入数组@newMarkers。 对于@oldMarkers 中的每个IMP 实例,都会将一个十进制数添加到@newMarkers。新的十进制数与前一个数字具有相同的“基本整数”,但添加了 0.1。换句话说,“IMP”的多个后续实例应该与最近读取的“chr”条目具有相同的整数,并附加一个十进制值,用于计算与该最近“chr”相对应的 IMP 的数量入口。

下面的脚本几乎可以 100% 运行。它甚至通常在以下情况下工作。在@oldMarkers 的某些地方,有许多 IMP 条目。当一行中有超过 10 个 IMP 时,代码应该将值推送到 @newMarkers 以便该条目块的所有“IMP”具有相同的整数,这也与对应于最近的数字匹配在@oldMarkers 中读取“chr”的实例。对该整数加 0.1。当小数点的值达到 0.9 时,小数点“重新开始”回到 .1 并从那里上升,直到 IMP 条目的结束。

例如,如果@oldMarkers 有一个由 13 个“IMP”组成的块并且是: chr1, chr2, IMP, IMP, IMP, IMP, IMP, IMP, IMP, IMP, IMP, IMP, IMP, IMP, IMP, chr2

那么@newMarkers 应该是: 1, 2, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.1, 2.2, 2.3, 2.4, 3

脚本摘要:

原始文件包含多行两个元素。第一个元素并不重要,因此在代码中被跳过。每行的第二个元素是一个 ID,类似于“chr4”或“IMP”。 while 循环读取每一行,将第二个元素添加到数组 @oldMarkers。

然后,这个数组被逐项读取。该脚本首先询问@newMarkers 中的条目是否对应于原始@oldMarker 列表中的“chr”或“IMP”。这是通过第一个 ifelse 集完成的。

接下来,对于这两个条件,进一步询问该条目是否来自对应于“chr”或“IMP”条目的数字本身。这是通过嵌入的ifelse 集与第一个此类集完成的。

然后根据条件定义新元素并将其推送到@newMarker。

就像我说的,这主要是有效的。然而,有时,当 IMP 的拉伸超过 10 时,脚本不会“回收”小数。相反,它将 .1 添加到前面的值并输入一个新的整数整数。但对于超过 10 的其他拉伸,它工作正常。与这个“错误”不一致。

你能发现问题吗?

my @oldMarkers = ();
my @newMarkers = ();

while ( my $line = <$FILE> )
    {
    chomp $line;
    my @entries = split( '\t', $line );
    push( @oldMarkers, $entries[ 1 ] ); 
    } ### end of while


for ( my $i = 0 ; $i < scalar @oldMarkers   ; $i++ )
    {  
     if ( $oldMarkers[ $i ] =~ m/chr/ ) ### is a marker
        {
         if ( $oldMarkers[ $i - 1 ] =~ m/IMP/ ) ### new marker comes after imputed site
            {
             push( @newMarkers, int( $newMarkers[ $i - 1 ] ) + 1 );            
            }

       else  ### is coming after a marker                                       
           {
            push( @newMarkers, $newMarkers[ $i - 1 ] + 1 ); 
           }    

      } ### if

   else    ### is an imputed site
      {
       if ( $oldMarkers[ $i - 1 ] =~ m/IMP/ ) ### imputed site is after another imputed site
          {
           my $value = $newMarkers[ $i - 1 ] - int( $newMarkers[ $i - 1 ] );

           if ( $value < .9 )
                {
                 push( @newMarkers, $newMarkers[ $i - 1 ] + .1 );   
                }

          elsif ( $value > .9 )
                {
                 push( @newMarkers, int( $newMarkers[ $i - 1 ] ) + .1  );   
                } 


        } ### if

   else ### imputed site is after a marker
        {
         push( @newMarkers, int( $newMarkers[ $i - 1 ] ) + .1 ); 
        }    

    } ### else   

} ### for    


print $newMarkerfile join( "\t", @newMarkers);             

【问题讨论】:

    标签: arrays perl loops rounding nested-loops


    【解决方案1】:

    仅使用整数算术来做到这一点会更容易和更可靠。基本上,跟踪两个整数值:一个用于. 之前的数字,另一个用于它之后的数字。如果.后面的位数达到10,则重置为1:

    my @newMarkers;
    my $chrCount = 0;
    my $impCount = 0;
    
    foreach my $marker (@oldMarkers) {
        if ( $marker =~ /^chr\d+$/ ) {
            $chrCount++;
            $impCount = 0;
            push @newMarkers, $chrCount;
        } elsif ( $marker eq "IMP" ) {
            $impCount++;
            $impCount = 1 if $impCount == 10;
            push @newMarkers, "$chrCount.$impCount";
        } else {
            die "Unrecognized marker $marker";
        }
    }
    

    (demo on codepad.org)

    【讨论】:

    • @eric: 不,它应该是++$chrCount,因为我们希望值之后增加。或者你可以做$chrCount++; push @newMarkers, $chrCount;。 (事实上​​,我只是编辑了我的答案来做到这一点。)
    • 谢谢!另外,假设我可能想在 $impCount 达到 10 时忽略它的重置。在这种情况下,当文件中有很长的 IMP 时,我会得到诸如 30.1 30.2 30.3 ... 30.9 30.10 30.11 ... 这样的值但是,在 excel 中, 30.130.10 的值看起来相同,30.1。你有办法打败它吗?
    • @eric:你也许可以通过push @newMarkers, sprintf '%d.%03d', $chrCount, $impCount;(或push @newMarkers, $chrCount + $impCount / 1000;)来获取30.00130.002等等。
    • 谢谢,我想我可以做的是将两个数组分开,将它们打印到单独的列中,然后在 excel 中将它们连接起来。
    【解决方案2】:

    10 × 0.1 = 1,但

    >perl -E"$x=0; $x += 0.1 for 1..10; say sprintf('%0.16f', $x); say int($x);"
    0.9999999999999999
    0
    

    在处理浮点数时,您应该始终使用某种形式、舍入或公差。

    太多的数字在二进制中是周期性的。你知道 1/3 在十进制中是如何周期性的吗?嗯,1/10 在二进制中是周期性的。 2/10、3/10、4/10、6/10、7/10、8/10 和 9/10 也是如此。这些数字都不能用浮点数无误地表示。

    【讨论】:

    • 你确定我有舍入错误吗?我只是想也许我做到了。如果我这样做了,你能找出代码的来源以及如何纠正它吗?
    • 我很肯定你在使用int,我很肯定这是一个错误。你要我指出int
    • @hobbs,...因为实际上你不能将 0.1 添加五次,是的。
    【解决方案3】:

    似乎工作正常:

    $imp_order = 0;
    $chr_order = 0;
    for my $old (@oldMarkers) {   
      if ( $old =~ m/chr/ ) ### is a marker
      {
    
        $imp_order = 0;
        $chr_order++;
    
        push( @newMarkers,  $chr_order );    
    
      } ### if
    
      else    ### is an imputed site
      {
          $imp_order = 0 if $imp_order == 9;
          $imp_order++;
          push( @newMarkers, $chr_order + $imp_order / 10 );   
    
      } ### else   
    
    } ### for    
    

    【讨论】:

      【解决方案4】:

      正如 ikegami 建议的那样,那些 int() 调用肯定会导致您的舍入问题。您可以使用 POSIX,然后根据需要使用 ceil() 或 floor() 来解决问题。

      在此处查看文档:http://perldoc.perl.org/perlfaq4.html#Does-Perl-have-a-round%28%29-function%3F-What-about-ceil%28%29-and-floor%28%29%3F-Trig-functions%3F

      例如,我认为您描述的确切错误可以通过替换来修复:

      elsif ( $value > .9 )
          {
              push( @newMarkers, int( $newMarkers[ $i - 1 ] ) + .1  );   
          }
      

      与:

      elsif ( $value > .9 )
          {
              push( @newMarkers, ceil( $newMarkers[ $i - 1 ] ) + .1  );   
          }
      

      您可能应该将所有这些 int() 调用替换为每种情况的适当舍入函数。

      跟进:我实际上更喜欢建议的多个解决方案,分别跟踪“chr”计数/订单和“imp”计数/订单,而不是作为单个浮点数。但我将把它留在这里,因为我认为它对发帖人有关如何实施四舍五入的解决方案具有指导意义。

      【讨论】:

        【解决方案5】:

        如果我理解正确,那么这就是所有必要的。

        use strict;
        use warnings;
        
        my @old = do {
          open my $fh, '<', 'markers.txt' or die $!;
          map /([^\t]+)$/, <$fh>;
        };
        
        my @new;
        my @marker;
        my $chr = 0;
        
        for (@old) {
          if ( /chr/ ) {
            @marker = (++$chr);
          }
          elsif ( @marker > 1 and $marker[1] == 9 ) {
            $marker[1] = 1;
          }
          else {
            $marker[1]++;
          }
          push @new, [@marker];
        }
        
        @new = map join('.', @$_), @new;
        
        print join(', ', @new), "\n";
        

        输出

        1, 2, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.1, 2.2, 2.3, 2.4, 3
        

        【讨论】:

          【解决方案6】:

          如果在您的第​​二个示例中,输出应该是: 1 2 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3 3.1 3.2 3.3 4

          而不是 > 使用 >=

          那么你有两个选择: int( $newMarkers[ $i - 1 ] ) + $value + .100000 或将 newMarkers[$i - 1] 的 int 值加 1

          【讨论】:

            猜你喜欢
            • 2014-08-21
            • 2010-10-31
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-04-03
            • 2018-03-05
            相关资源
            最近更新 更多