perl:查找大数的均值和方差而不会溢出

perl: Finding mean and variance of large numbers without overflow

我正在使用子例程 (stats) 来计算数字列表的统计信息。 如果存储为普通 perl 数字,这些数字可能大到足以失去精度。 我收到 JSON formatted strings 这样的数字。 要在不损失精度的情况下解码这些字符串, 我使用激活了 allow_nonrefallow_bignumJSON::PP 对象。 我将此类解码数字的列表发送到 stats 子例程 (参见下面显示的代码)。 这个例程计算一些统计数据。 然后将这些统计信息编码为 JSON 并保存到文件中。

大多数时候该过程似乎工作正常,但是 对于某些输入(示例见代码)均值和方差统计的计算值 要么明显错误,要么被编码器编码为 JSON 字符串,或两者兼而有之。 我怀疑这是由于 JSON 解码创建的 Math::BigIntMath::BigFloat 对象的交互,以及 List::Util::sum0.

我正在尝试找出导致这种情况的原因以及 avoid/fix 的方法, 最好不要求助于大的非核心模块。 我愿意接受不精确的均值和方差计算, 但并非完全不准确的结果 或在 JSON.

中编码为字符串的数值结果

演示问题的脚本 (stats.pl):

use strict;
use warnings;

use Data::Dumper;
$Data::Dumper::Varname = "DUMPED_RAWDATA";
use JSON::PP;
use List::Util;

my $JSON = JSON::PP->new->allow_bignum->utf8->pretty->canonical;

sub stats {

    #TODO fix bug about negative variance. AVOID OVERFLOW
    #TODO use GMP, XS?
 
    # @_ has decoded numbers (called RAWDATA here)
    my $n    = scalar @_;
    my $sum  = List::Util::sum0(@_);
    my $mean = $sum / $n;
    my $var  = List::Util::sum0( map { $_**2 } @_ ) / $n - $mean**2;

    my $s = {
        n        => $n,
        sum      => $sum,
        max      => List::Util::max(@_),
        min      => List::Util::min(@_),
        mean     => $mean,
        variance => $var
    };
    # DUMP STATE IF SOME ERROR OCCURS
    print Dumper( \@_ ),
      $JSON->encode( { json_encoded_stats => $s, json_encoded_rawdata => \@_ } )
      if ( '"' eq substr( $JSON->encode($var), 0, 1 ) #MEAN ENCODED AS STRING
        or '"' eq substr( $JSON->encode($mean), 0, 1 ) #VARIANCE ENCODED AS STRING
        or $var < 0 ); #VARIANCE IS NEGATIVE!
    $s;
}

my @test = (
    [
        qw( 919300112739897344 919305709216464896 919305709216464896 985592115567603712 959299136196456448)
    ],
    [qw(479655558 429035600 3281034608 3281034608 2606592908 3490045576)],
    [ qw(914426431563644928) x 3142 ]
);
for (@test) {
    print "---\n";
    stats( map { $JSON->decode($_) } @$_ );
}

下面是 perl stats.pl 的缩减输出,问题显示为 <---

---
$DUMPED_RAWDATA1 = [
                     '919300112739897344',
                     '919305709216464896',
                     '919305709216464896',
                     '985592115567603712',
                     '959299136196456448'
                   ];
{
   "json_encoded_rawdata" : [
      919300112739897344,
      919305709216464896,
      919305709216464896,
      985592115567603712,
      959299136196456448
   ],
   "json_encoded_stats" : {
      "max" : 985592115567603712,
      "mean" : "9.40560556587377e+17", <--- ENCODED AS STRING
      "min" : 919300112739897344,
      "n" : 5,
      "sum" : 4702802782936887296,
      "variance" : 7.46903843214008e+32
   }
}
---
$DUMPED_RAWDATA1 = [
                     479655558,
                     429035600,
                     3281034608,
                     3281034608,
                     2606592908,
                     3490045576
                   ];
{
   "json_encoded_rawdata" : [
      479655558,
      429035600,
      3281034608,
      3281034608,
      2606592908,
      3490045576
   ],
   "json_encoded_stats" : {
      "max" : 3490045576,
      "mean" : 2261233143,
      "min" : 429035600,
      "n" : 6,
      "sum" : 13567398858,
      "variance" : "-1.36775568782523e+18" <--- NEGATIVE VARIANCE, STRING ENCODED
   }
}
---
$DUMPED_RAWDATA1 = [
                     '914426431563644928',
             .
             .
             .
             <snip 3140 identical lines>
                     '914426431563644928'
                   ];
{
   "json_encoded_rawdata" : [
      914426431563644928,
      .
      .
      .
      <snip 3140 identical lines>
      914426431563644928
   ],
   "json_encoded_stats" : {
      "max" : 914426431563644928,
      "mean" : "9.14426431563676e+17", <--- STRING ENCODED
      "min" : 914426431563644928,
      "n" : 3142,
      "sum" : 2.87312784797307e+21,
      "variance" : -9.75463826617761e+22 <--- NEGATIVE VARIANCE
   }
}

None 的输入大到需要 JSON::PP 在具有 64 位整数的系统上创建 Math::BigInt 对象,所以它不需要。

您可以在子程序的开头执行类似以下操作。

@_ = map { Math::BigInt->new($_) } @_;   # Or ::BigFloat?

或者,

my $zero_B = Math::BigInt->new(0);

sub stats {
    my $n      = @_;
    my $sum_B  = sum($zero_B, @_);
    my $mean_B = $sum_B / $n;
    my $var_B  = sum( map { Math::BigInt->new($_) ** 2 } @_ ) / $n - $mean_B ** 2;
    my ($min, $max) = minmax(@_);
    return {
        n        => $n,
        sum      => $sum_B,
        max      => $max,
        min      => $min,
        mean     => $mean_B,
        variance => $var_B,
    };
}

总计:

use strict;
use warnings;

use Data::Dumper    qw( Dumper );
use JSON::PP        qw( );
use List::MoreUtils qw( minmax );
use List::Util      qw( sum );
use Math::BigInt    qw( );

my $zero_B = Math::BigInt->new(0);
my $JSON = JSON::PP->new->allow_bignum->utf8->pretty->canonical;

sub stats {
    my $n      = @_;
    my $sum_B  = sum($zero_B, @_);
    my $mean_B = $sum_B / $n;
    my $var_B  = sum( map { Math::BigInt->new($_) ** 2 } @_ ) / $n - $mean_B ** 2;
    my ($min, $max) = minmax(@_);
    return {
        n        => $n,
        sum      => $sum_B,
        max      => $max,
        min      => $min,
        mean     => $mean_B,
        variance => $var_B,
    };
}

my @test = (
    [qw( 919300112739897344 919305709216464896 919305709216464896 985592115567603712 959299136196456448 )],
    [qw( 479655558 429035600 3281034608 3281034608 2606592908 3490045576 )],
    [ qw( 914426431563644928 ) x 3142 ]
);

for (@test) {
    print "---\n";
    my $s = stats( map { $JSON->decode($_) } @$_ );

    if (
           $JSON->encode($s->{variance}) =~ /"/  # MEAN ENCODED AS STRING
        || $JSON->encode($s->{mean}) =~ /"/      # VARIANCE ENCODED AS STRING
        || $s->{variance} < 0                    # VARIANCE IS NEGATIVE!
    ) {
        local $Data::Dumper::Varname = "DUMPED_RAWDATA";
        print Dumper($_);
        print $JSON->encode({
            json_encoded_rawdata => $_,
            json_encoded_stats => $s,
        });
    } else {
        print "ok\n";
    }
}

备注:

  • 即使对象已经是 Math::* 对象,这两种方法都有效。
  • 为了清楚起见,我使用 _B 确定变量保证包含 Math:Big* 对象。
  • 我将测试代码移到了测试工具中。
  • 我使用 minmax 因为它比分别调用 minmax 更有效。
  • 我从模块中导入了 subs 以避免使用他们的全名。
  • 无需将标量上下文中的某些内容强制转换为标量上下文。

@ikegami 的回答正确 但是这个子程序对我来说太慢了 在我的程序的内部循环中被调用了很多次。 我认为这是确保 所有数字都转换为任意精度的数字。 我最终使用了以下实现 这避免了将所有数字转换为任意数字 精度类型。

sub stats {

    my $n = scalar @_;
    my $sum    = List::Util::sum0(@_);
    my $mean   = $sum / $n;
    my $var = List::Util::sum0( map { ( $_ - $mean )**2 } @_ ) / $n;
    $mean   += 0;
    $var += 0;    # TO ENSURE THAT THEY ARE ENCODED AS NUMBERS IN JSON
    {
        n      => $n,
        sum    => $sum,
        max    => List::Util::max(@_),
        min    => List::Util::min(@_),
        mean   => $mean,
        variance => $var,
    };
}

我改变了计算方差的方法 确保避免负面结果 (正如@Robert所建议的)。 它可能会牺牲 $sum 中的精度 (以及所有依赖于 $sum 的东西) 由于大整数的浮点加法。 不过,它在可接受的执行时间内完成了作业。

意外 JSON 将数字编码为字符串 在 https://metacpan.org/pod/JSON::PP#simple-scalars 中有解释。 这个问题用方法解决了 建议在那里强制编码为数字。

JSON::PP will encode undefined scalars as JSON null values, scalars that have last been used in a string context before encoding as JSON strings, and anything else as number value

You can force the type to be a JSON number by numifying it:

my $x = "3"; # some variable containing a string
$x += 0;     # numify it, ensuring it will be dumped as a number
$x *= 1;     # same thing, the choice is yours. in to force