LupoX has asked for the wisdom of the Perl Monks concerning the following question:

Hello acolytes, scribes, monks, friars, abbots, bishops, pontiffs and saints,

The Problem:

I want to generate a list of random values. I want to specify 4 things:

  • How many numbers to generate
  • The minimum value of the generated numbers
  • The maximum value of the generated numbers
  • An average number that should match the avarage of the generated values
  • This is my basic approach:

    #!/usr/bin/perl -w use strict; my $mystart = 0; # What is the minimum value of random numbers you +want to generate my $myend = 100; # What is the maximum value of the random numbers +to generate my $count = 50; # How many numbers should be generated my $my_average = 50; # What is the decided avarage of the generated + numbers my $number; my @number; my $summ = 0; my $i = 0; my $real_average; print "\nList of random values:\n"; my $start = $mystart; my $end = $myend; while ($i < $count) { $number = rand ($end); if ($number >= $start) { @number = split(/\./, $number); # get integer of float $i++; $summ = $summ + $number[0]; $real_average = $summ / $i; print "Random number: $number[0]\n"; print "Average: $real_average\n\n"; } if ($my_average < $real_average) { # if real avarage is bigger than decided avarage # allow only smaller random numbers $end = $myend / 2; $start = $mystart; } elsif ($my_average > $real_average) { # if real avarage is smaller than decided # allow only bigger random numbers $end = $myend; $start = $myend / 2; } }

    All critic, input, code samples, good wishes, etc.. is highly welcome!

    All the best for you...LupoX

    Replies are listed 'Best First'.
    Re: list of random numbers with given avarage
    by jepri (Parson) on Mar 12, 2002 at 12:28 UTC
      Cool. We used to do this in Physics (undergrad, I hasten to add) when we couldn't get lab results, but we knew what the 'perfect' results were.

      You are doing it the hard way. Rather than painfully adjusting your random number function, just fake the random numbers using a noise function. If you do a web search for 'Gaussian' or 'Lorentzian' you should get something useful for random functions.

      ____________________
      Jeremy
      I didn't believe in evil until I dated it.

        <Humor>
        *Sigh* Typical physicists.

        I think they all subscribe to the "Measure with micrometer, mark with chalk, cut with axe" method.

        My roommate in college (now a Phd physics student) used to say "It's only a power of ten, that's well within our desired range".

        GAH!!</Humor>

        C-.

    Re: list of random numbers with given avarage
    by ChOas (Curate) on Mar 12, 2002 at 12:21 UTC
      Hi!,

      Sorry... not much time, have to go to a meeting, but I
      whiped something up quickly

      I`ll have another look later, but I like this one:
      #!/usr/bin/perl -w use strict; my $Min=0; my $Max=100; my $Count=50; my $Average=50; my $RealAverage=0; my $Total=0; my @Result; for(1..$Count) { #my $New=int(rand($Max/2)+$Min+(($RealAverage>$Average)||$Average)); # Sorry, this line was totally screwed up, as it would # give results outside the bounds given, substitute by # this: my $New=int(rand($Max-$Min))+$Min; $New+=$Average if ((($New+$Average)<$Max)&&($RealAverage<$Average)); $New-=$Average if ((($New-$Average)>$Min)&&($RealAverage>$Average)); $Total+=$New; $RealAverage=$Total/$_; push @Result,$New; }; print join "\n",@Result; print "\nAverage: $RealAverage\n";

      GreetZ!,
        ChOas

      print "profeth still\n" if /bird|devil/;
    Re: list of random numbers with given avarage
    by YuckFoo (Abbot) on Mar 12, 2002 at 15:34 UTC
      Fun problem, here's a suggestion and a solution.

      Your algorithm doesn't work very well if the desired average is near your minimum or maximum, like from 0 - 100, desired average = 10.

      If you want random numbers from $start to $end, instead of $number = rand($end) and testing to see if it's bigger than $start, use $number = rand($end - $start) + $start.

      This program will either choose a random number from $start to $average or from $average to $end, based on whether the actual average is lesser or greater than the target average.

      I have no idea what the distribution will look like, or what you want the distribution to look like.

      YuckFoo

      #!/usr/bin/perl use strict; my ($MIN, $MAX, $NUM, $AVERAGE) = qw(0 100 64 32); my $small = $AVERAGE - $MIN; my $large = $MAX - $AVERAGE; my ($i, $rand, $total, $actual); for ($i = 1; $i <= $NUM; $i++) { if ($actual < $AVERAGE) { $rand = int(rand($large) + $AVERAGE); } else { $rand = int(rand($small) + $MIN); } $total += $rand; $actual = $total / $i; print "$rand $actual\n"; }
    Re: list of random numbers with given avarage
    by johannz (Hermit) on Mar 12, 2002 at 16:58 UTC

      I rewrote your code and made a stab at my own.

      Some of the changes I made to your code:

      • No empty cycles without outputing a random number.
      • No possibilities of infinite loop.
        • could happen if ($myend/2 < $mystart)
        • example: mystart = 60, myend = 100).
      • Actually use int function, not a split on a period
      • Use += instead of $sum = $sum + $number

      In my code, instead of only choosing numbers from part of the range, I weigh the results smaller or larger as needed. This is an advantage if the desired average is close to the real average, but a disadvantage if the desired average is radically different. The reason I consider it an advantage when the desired average is close to the real average is that you still could get the entire ranger of numbers; not a sequence of numbers below the average, then a sequence of numbers above the average, etc.

      use strict; use warnings; # Comment out the tests you don't want to run. my @choices; @choices = (0, 100, 50, 50); @choices = (25, 75, 50, 50); @choices = (0, 100, 50, 10); my @numbers = his_method(@choices); #my @numbers = my_method(@choices); print "Results:\n@numbers\n"; exit; sub his_method { # re-written my $mystart = shift; my $myend = shift; my $count = shift; my $my_average = shift; my ($start, $end) = ($mystart, $myend); my ($number, @numbers); my ($sum, $real_average) = 0; print "\nList of random values:\n"; for (my $i = 1; $i <= $count; $i++) { $number = int(rand($end+1)) + $start; push @numbers, $number; $sum += $number; $real_average = $sum/$i; print "Random number $i: $number ($start, $end)\n"; print "Average: $real_average\n\n"; ($start, $end) = ($real_average < $my_average) ? # make larger ( $my_average, $myend-$my_average) : ($real_average > $my_average) ? # make smaller ( $mystart, $my_average - $mystart) : # if no adjustment needed. ( $mystart, $myend); } return @numbers; } sub my_method { my $mystart = shift; my $myend = shift; my $count = shift; my $my_average = shift; my ($number, @numbers); my ($sum, $real_average) = 0; my $next_adj = 1; print "\nList of random values:\n"; for (my $i = 1; $i <= $count; $i++) { $number = int(rand()**$next_adj * ($myend+1)) + $mystart; push @numbers, $number; $sum += $number; $real_average = $sum/$i; print "Random number $i: $number ($next_adj)\n"; print "Average: $real_average\n\n"; $next_adj = ($real_average < $my_average) ? # make larger .5 # so take square root of fraction : ($real_average > $my_average) ? # make smaller 2 # so take power of 2 : # if no adjustment needed. 1; } return @numbers; }
    Re: list of random numbers with given avarage
    by belg4mit (Prior) on Mar 12, 2002 at 15:33 UTC
      A simpler recipe would be f(average, range). Pseduo-code: sub f{rnd(range)-range/2+average;}

      --
      perl -pe "s/\b;([st])/'\1/mg"

        This could result in values outside the allowed values.

        Example:

        bottom = 0
        top = 100
        range = bottom - top = 100
        desired average = 25
        which would result in
        sub f{rand(100) - 50 + 25}
        which is equivalent to
        sub f{rand(100) - 25}
        As you can see, 25% percent of the time, you would get a result that was out of the allowed bounds.
          Umm not exactly, I guess I didn't make myself clear. My proposed algorythm *does not* meet the same requirements, namely top and bottom. It instead provides what I imagine the original algorythm would most likely be used for which is providing "random" values centered around a given value (the average).

          UPDATE: Note this simpler method makes more sense for the physicists as well. The other algorythm produces many outliers which ought to be discarded in a real data set.

          Average=50, Range=10
          
          Average:        Avg - Rng/2:      Avg - Rng/2 + rnd(Rng):
          45----50----55  45----50----55    45----50----55
                 *         *                 ????? ??????
          
          Since rnd can return anything between 0 (minimum) or Range (maximum) we get a full spread. It may also be interesting to note that this is equivalent to sub f{average+range/2-rnd(range)}

          --
          perl -pe "s/\b;([st])/'\1/mg"

    Re: list of random numbers with given avarage
    by Zaxo (Archbishop) on Mar 12, 2002 at 21:46 UTC

      Your requirements are met most naturally by using another probability distribution than the uniform one rand is crafted to deliver. The requirement of a lower bound $Nl is easily dealt with by just considering the interval 0..($Nu-$Nl) and adding $Nl to all results. The requirement for an upper bound restricts us to distributions with a finite range. The requirement for a specified mean forces us to look at distributions with more parameters than rand provides.

      The ad-hoccery one's tempted into, simply filtering or adjusting a uniform distribution, will lead to 'random' numbers with bizarre statistical properties.

      Abramowitz and Stegun reveals one standard probability distribution for continuous finite intervals, the beta disribution, and two for integers, the binomial distribution and the hypergeometeric.distribution. All three have enough adjustable parameters to satisfy your requirements.

      Both integer distributions make use of the binomial coefficients <wish cap="MathMl"/>, so I'll give a simple recursive implementation:

      sub binomial { my ($n,$s)=@_; $_[1]<=$_[0]/2 ? $_[1] ? 0 | ($_[0] - $_[1] + 1) * binomial( $_[0], $_[1] - 1) / +$_[1] : 1 : $_[1] != $_[0] ? 0 | ($_[1] + 1) * binomial( $_[0], $_[1] + 1) / ($_[0] - + $_[1]) : 1; }
      This is not very efficient, but it staves off large intermediate numbers and avoids copying as much as possible in a recursive definition. You may wish to do this using &Math::BigInt::bfac instead.

      The plan is to make a largeish array of numbers from the interval, each represented in proportion to its probability, and pick numbers from that array with rand as index. For the binomial distribution:

      # Usage: # my @probs = prob_binomial( $span, $avg); # these are 0 based arguments, subtract the min you want to get them. sub prob_binomial { my ($max, $avg) = @_; my $p = $avg / $max; map {binomial($max, $_) * $p**$_ * (1-$p)**($max-$_)} 0..$max; } my $min = 6; my $max = 21; my $avg = 17; my $num = 40; my @problist = prob_binomial($max-$min,$avg-$min); # using ~1000 element array, adjust to suit ranges my @picklist = map {($min+$_) x int( .5 + 1000 * $problist[$_])} 0..$m +ax-$min; print $picklist[rand @picklist],$" for 1..$num;
      For the hypergeometric distribution you get a free extra parameter you can use to tweak your results. this is its point probability function, in terms of three integers $N1 < $N2 and $n with $n < $N1+$N2:
      sub prob_hyper { my ($N1,$N2,$n)=@_; my $span = $N1<$n? $N1 : $n; map {binomial($N1,$_) * binomial($N2,$n-$_) / binomial($N1+$N2,$n) +} 0..$span; }
      This has the great advantage that the relative probabilities can be made integers by making its @picklist binomial($N1+$N2,$n) in length. Then your results will be exact.

      Note that these methods don't force a particular list of randoms to take on the average. That will happens in the long run, it's all statistics! Enjoy.

      Update: Forgot to mention, the mean for the hypergeometric distribution is $n*$N1/($N1+$N2).

      After Compline,
      Zaxo

    Re: list of random numbers with given avarage
    by thraxil (Prior) on Mar 12, 2002 at 16:57 UTC
    Re: list of random numbers with given average
    by I0 (Priest) on Mar 13, 2002 at 04:17 UTC
      #!/usr/bin/perl -w use strict; my $mystart = 0; # What is the minimum value of random numbers + you want to generate my $myend = 100; # What is the maximum value of the random num +bers to generate my $count = 50; # How many numbers should be generated my $my_average = 50; # What is the decided avarage of the generate +d numbers my $number; my @number; my $summ = 0; my $i = 0; my $real_average; print "\nList of random values:\n"; for $i(1.. $count ){ my $start = $mystart; my $end = $myend; my $more = ($my_average*$count-$summ); my $average = $more/(1+$count-$i); if( ($count-$i)*$myend < $more-$mystart ){ $start = $more-($count-$i)*$myend; } if( ($count-$i)*$mystart > $more-$myend ){ $end = $more-($count-$i)*$mystart; } $number = random($start,$end,$average); $summ += int $number; $real_average = $summ / $i; print "Random number: $number\n"; print "Average: $real_average\n\n"; } sub random{ my($start,$end,$average) = @_; # print "$start,$end,$average\n"; if( $start >= $end ){ return $average; } my $b = ($average-$start)/($end-$start); if( 0 ){ my $p = $b<=.5?1/$b-1:(1-$b)/$b; #Update: this was silly return $start + ($end-$start)*(rand)**$p; }else{#Update: smoother skew function return $b >= 0.5 ? $start + ($end-$start)*(rand)**(1/$b-1) : $end + ($start-$end)*(rand)**(1/(1-$b)-1) ; } }

        A GREAT THANK YOU to all perlmonks for your input. Again and again I am impressed by the Perl Community!

        scribe LupoX

    Re: list of random numbers with given avarage
    by pizza_milkshake (Monk) on Mar 13, 2002 at 01:41 UTC
      this conforms to what you were talking about. gets flakier the closer avg gets to min || max. comments?
      #!/usr/bin/perl -w use strict; use Data::Dumper; my ($acceptable_laziness) = 1.0e-15; sub rand_list { my ($howmany, $min, $max, $avg, @a) = (@_, ()); print STDERR "ERROR: Make sure (min < avg < max)\n", return if ($avg + <= $min || $avg >= $max); push @a, rand($max-$min)+$min for 1..$howmany; # nudge list toward avg w/o pushing numbers above/below min/max while (avg_list(@a) != $avg){ my ($nudge, $tmp) = (avg_list(@a)-$avg, undef); last if (abs $nudge < $acceptable_laziness); for (@a){ $tmp = ($_ - $nudge); $_ = $tmp if ($min <= $tmp && $max >= $tmp); } } @a; } sub avg_list { my ($c, $t) = (0, 0); $t+=pop, $c++ while @_; ($t / $c) if $c } # 20 nums between 1 and 5 that average to 3 my @b = rand_list(20, 1, 5, 3); print Dumper(\@b);
      perl -e'@a=split//," \n/)\"={\"";print $a[$_] for split//,"00002731000223155264555102401"'
    Re: list of random numbers with given avarage
    by pizza_milkshake (Monk) on Mar 13, 2002 at 01:29 UTC
      #!/usr/bin/perl -w use strict; use Data::Dumper; sub rand_list { my ($howmany, $min, $max, @a) = (@_, ()); push @a, rand($max-$min)+$min for 1..$howmany; @a; } sub avg_list { my ($c, $t) = (0, 0); $t+=pop, $c++ while @_; ($t / $c) } my @b = rand_list(100, 1, 5); # 100 nums between 1 and 5 print Dumper(\@b);
      perl -e'@a=split//," \n/)\"={\"";print $a[$_] for split//,"00002731000223155264555102401"'