Generating a random number with weighted probability - 'Distribution' gem - ruby

I would like to create a random number generator, that generates a random decimal number:
Greater than 0.0
Less than 15.0
Where the probability of that number being close to 2.0 is relatively high
The probability of it being near 15.0 or very close to zero is very low
I'm terrifically poor at mathematics but my research seems to tell me I want to pull a random number from a Cumulative Distribution Function resembling a Fisher–Snedecor (F) pattern, a bit like this one:
http://cdn.app.compendium.com/uploads/user/458939f4-fe08-4dbc-b271-efca0f5a2682/742d7708-efd3-492c-abff-6044d78e3bbd/Image/6303a2314437d8fcf2f72d9a56b1293a/f_distribution_probability.png
I am using a Ruby gem called Distribution (https://github.com/sciruby/distribution) to try and achieve this. It looks like the right tool, but I'm having a terrible time trying to understand how to use it to achieve the desired outcome :( Any help please.

I'll take it back, there is no rng call for F. So, if you want to use Distribution gem, what I would propose is to use Chi2 with 4 degrees of freedom.
Mode for Chi2 with k degress of freedom is equal to k-2, so for 4 d.f. you'll get mode at 2, see here. My Ruby is rusty, bear with me
require 'distribution'
normal = Distribution::Normal.rng(0)
g1 = normal.call
g2 = normal.call
g3 = normal.call
g4 = normal.call
chi2 = g1*g1 + g2*g2 + g3*g3 + g4*g4
UPDATE
You have to truncate it at 15, so if generated chi2 is greater than 15 just reject it and generate another one. Though I would say you won't see a lot of
value above 15, check graphs for PDF/CDF.
UPDATE II
And if you want to get samples from F, make generic Chi2 generator for d degrees of freedom from code above, and just sample ratio of chi2, check here
chi2_d1 = DChi2(d1)
chi2_d2 = DChi2(d2)
f = (chi2_d1.call / d1) / (chi2_d2.call / d2)
UPDATE III
And, frankly, I don't see how you could get F distribution working for you. It is ok at 0, but mode is equal to (d1-2)/d1 * d2/(d2 + 2), and it is hard to see it equal to 2. Graph you provided has mode at about 1/3.

Here's a very crude, unscientific, non-mathy attempt at using the F-distribution with the parameters you gave in the F-function image (3 and 36).
First I calculate what F-value is needed for the CDF to be 0.975 (100% - 2.5% for the upper end of the range for your number 15):
To calculate that we can use the p_value method like so:
> F_15 = Distribution::F.p_value(0.975, 3, 36)
=> 3.5046846420861977
Next we simply use a multiplier so that when we calculate the CDF it will return the value 15 when the F-value is F_15.
> M = 15 / F_15
=> 4.27998565687528
And now we can generate random numbers with rand, which has a range of 0..1 like so:
[M * Distribution::F.p_value(rand, 3, 36), 15].min
The question is will this function be close to the number 2 with a 45% probability? Well..sort of. You need to pick the right parameters for the F-distribution to tweak the curve (or just adjust the multiplier M). But here's a sample with the parameters from your image:
0.step(0.99, 0.02).map { |n|
sprintf("%0.2f", M * Distribution::F.p_value(n, 3, 36))
}
Gives you:
["0.00", "0.26", "0.42", "0.57", "0.70", "0.83", "0.95", "1.07",
"1.20", "1.31", "1.43", "1.55", "1.67", "1.80", "1.92", "2.04",
"2.17", "2.30", "2.43", "2.56", "2.70", "2.84", "2.98", "3.13",
"3.28", "3.44", "3.60", "3.77", "3.95", "4.13", "4.32", "4.52",
"4.73", "4.95", "5.18", "5.43", "5.69", "5.97", "6.28", "6.61",
"6.97", "7.37", "7.81", "8.32", "8.90", "9.60", "10.45", "11.56",
"13.14", "15.90"]

Sometimes you know which distribution applies because of the nature of the data. If, for example, the random variable is the sum of independent, identical Bernouli (two-state) random variables, you know the former has a binomial distribution, which can be approximated by a Normal distribution. When, as here, that does not apply, you can use a continuous distribution, shaped by it's parameters, or simply use a discrete distribution. Others have made suggestions for using various continuous distributions, so I'll pass on some remarks about using a discrete distribution.
Suppose the discrete probability density function were the following:
pdf = [[0.5, 0.03], [1.0, 0.06], [1.5, 0.10], [ 2.0, 0.15], [2.5 , 0.15], [ 3.0, 0.10],
[4.0, 0.11], [6.0, 0.14], [9.0, 0.10], [12.0, 0.03], [14.0, 0.02], [15.0, 0.01]]
pdf.map(&:last).reduce(:+)
#=> 1.0
This could be interpreted as there being a probability of 0.03 that the random variable will be less than 0.5, a 0.06 probability of the random variable being greater than or equal 0.5 and less than 1.0, and so on.
A discrete pdf might be constructed from historical data or by sampling, an advantage it has over using a continuous distribution. It can be made arbitrarily fine by increasing the numbers of intervals.
Next convert the pdf to a cumulative distribution function:
cum = 0.0
cdf = pdf.map { |k,v| [k, cum += v] }
#=> [[0.5, 0.03], [1.0, 0.09], [1.5, 0.19], [2.0, 0.34], [2.5, 0.49], [3.0, 0.59],
# [4.0, 0.7], [6.0, 0.84], [9.0, 0.94], [12.0, 0.97], [14.0, 0.99], [15.0, 1.0]]
Now use Kernel#rand to generate pseudo random variates between 0.0 and 1.0 and use Enumerable#find to associate the random variate with a cdf key:
def rnd(cdf)
r = rand
cdf.find { |k,v| r < v }.first
end
Note that cdf.find { |k,v| rand < v }.first would produce erroneous results, since rand is executed for each key-value pair of cdf.
Let's try it 100,000 times, recording the relative frequencies
n = 100_000
inc = 1.0/n
n.times.with_object(Hash.new(0.0)) { |_, h| h[rnd(cdf)] += inc }.
sort.
map { |k,v| [k, v.round(5)] }.to_h
#=> { 0.5=>0.03053, 1.0=>0.05992, 1.5=>0.10084, 2.0=>0.14959, 2.5=>0.15024,
# 3.0=>0.10085, 4.0=>0.10946, 6.0=>0.13923, 9.0=>0.09919, 12.0=>0.03073,
# 14.0=>0.01931, 15.0=>0.01011}

Related

Generating a series of non-random numbers with a maximum sum of 100

I am working on a program that has a component that will generate simulated demographic numbers for various hypothetical jurisdictions.
The methods I have set up to generate each subset of demographics are dependent on some other variables, but generally, most jurisdictions are supposed to look something like:
White - 65%
Latino - 20%
African-American - 10%
Other - 5%
Of course, this isn't always the case. In some scenarios, white may be well under 50% with either Latino or AA being the most significant, but those are more edge cases. But in general that's usually about the balance.
So I am trying to figure out how to generate each demographic, which again is fed from different variables, mostly independently, but ensuring the number always adds up to 100.
I had thought about generating white first, since it's typically the largest, and then just creating a generator where Latino% = 100 - white%*.35 (.35 is just an example here), and so forth, but this creates a situation in which white would always be the plurality, which I don't necessarily want to happen.
So I am a bit stuck. I imagine this is as much a math problem as a Ruby problem. As a non-math person (who, as they have delved into programming, wishes they had paid better attention in class), I'd appreciate any guidance here.
Thank you!
First specify a cumulative distribution function (CDF).
DIST = { white: 0.65, latino: 0.85, aa: 0.95, other: 1.00 }
Note that
DIST[:white] - 0 #=> 0.65
DIST[:latino] - DIST[:white] #=> 0.20
DIST[:aa] - DIST[:latino] #=> 0.10
DIST[:other] - DIST[:aa] #=> 0.05
Now create a method to (pseudo-) randomly select one person from the population and return their ethnicity.
def select_one
rn = rand
DIST.find { |_k, v| rn <= v }.first
end
Try it.
10.times { p select_one }
:white
:aa
:latino
:white
:white
:white
:white
:white
:white
:latino
Now write a method to return a random sample of size n.
def draw_sample(n)
n.times.with_object(Hash.new(0)) { |_, h| h[select_one] += 1 }
end
Try it.
10.times { p draw_sample(100) }
{:white=>66, :latino=>21, :aa=>9, :other=>4}
{:white=>72, :latino=>14, :aa=>11, :other=>3}
{:white=>61, :latino=>19, :aa=>14, :other=>6}
{:white=>64, :latino=>25, :aa=>8, :other=>3}
{:white=>69, :latino=>19, :aa=>4, :other=>8}
{:white=>68, :latino=>17, :aa=>9, :other=>6}
{:white=>68, :latino=>16, :aa=>12 :other=>4}
{:white=>51, :latino=>27, :aa=>10, :other=>12}
{:white=>69, :latino=>23, :aa=>6, :other=>2}
{:white=>63, :latino=>19, :aa=>14, :other=>4}
(Note the order of the keys above varied; I reordered them to improve readability.)
On could alternatively write
def draw_sample(n)
n.times.map { select_one }.tally
end
though this has the disadvantage that it creates an intermediate array.
See Kernel#rand, the form of Hash::new that takes an argument (the default value, here zero) and Enumerable#tally.
From what I understand, each demographic depends on some external variables. What you could do then is
whites = getWhites(); // could be anything really
aa = getAA();
latinos = getLatinos();
sum = whites + aa + latinos;
whites = whites / sum * 100;
aa = aa / sum * 100;
latinos = latinos / sum * 100;
This guarantees that they always sum up to 100
Edit: The code is pseudocode (not ruby), assuming floating point data type

How do I select a random key from a hash using the probability distribution stored within the corresponding values?

When I create a new opal, I want to randomly assign it one of many possible features. However, I want some qualities to be more common than others. I have a hash with possible features and their relative probability (out of a total of 1).
How do I choose a feature at random, but weighted according to the probability?
'possible_features':
{
'white_pin_fire_green': '0.00138',
'white_pin_fire_blue': '0.00138',
'white_pin_fire_yellow': '0.00144',
'white_pin_fire_purple': '0.00144',
'white_pin_fire_pink': '0.00036',
'white_straw_green': '0.01196',
'white_straw_blue': '0.01196',
'white_straw_yellow': '0.01248',
'white_straw_purple': '0.01248',
'white_straw_pink': '0.00312',
'white_ribbon_green': '0.01196',
'white_ribbon_blue': '0.01196',
'white_ribbon_yellow': '0.01248',
'white_ribbon_purple': '0.01248',
'white_ribbon_pink': '0.00312',
'white_harlequin_green': '0.0069',
'white_harlequin_blue': '0.0069',
'white_harlequin_yellow': '0.0072',
'white_harlequin_purple': '0.0072',
'white_harlequin_pink': '0.0018',
'white_no_fire': '0.06',
'black_pin_fire_green': '0.00552',
'black_pin_fire_blue': '0.00552',
'black_pin_fire_yellow': '0.00576',
'black_pin_fire_purple': '0.00576',
'black_pin_fire_pink': '0.00144',
'black_straw_green': '0.04784',
'black_straw_blue': '0.04784',
'black_straw_yellow': '0.04992',
'black_straw_purple': '0.04992',
'black_straw_pink': '0.01248',
'black_ribbon_green': '0.04784',
'black_ribbon_blue': '0.04784',
'black_ribbon_yellow': '0.04992',
'black_ribbon_purple': '0.04992',
'black_ribbon_pink': '0.01248',
'black_harlequin_green': '0.0276',
'black_harlequin_blue': '0.0276',
'black_harlequin_yellow': '0.0288',
'black_harlequin_purple': '0.0288',
'black_harlequin_pink': '0.0072',
'black_no_fire': '0.24'
}
For example, if I randomly generate 100 opals, I'd like for approximately 24 of them to have the "black_no_fire" feature.
Thank you for any help!
If I can assume that the hash values do indeed add up to exactly 1.0, then the solution is little simpler. (Otherwise, this approach would still work, but requires a little extra effort to first sum all the values - and use them as a weighting, but not a direct probability.)
First, let's choose a random value between 0 and 1, to represent a "fair selection". You may wish to use SecureRandom.random_number in your implementation.
Then, I loop through the possibilities, seeing when the cumulative sum reaches the chosen value.
possible_features = {
white_pin_fire_green: "0.00138",
white_pin_fire_blue: "0.00138",
# ...
}
r = rand
possible_features.find { |choice, probability| (r -= probability.to_f) <= 0 }.first
This effectively treats each possibility as covering a range: 0 .. 0.00138, 0.00138 .. 0.00276, 0.00276 .. 0.00420, ..., 0.76 .. 1.
Since the original random value (r) is was chosen from an even distribution, its value will lie within one of those ranges with the desired weighted probability.
Suppose your hash were as follows.
pdf = {
white_pin_fire_green: 0.21,
white_pin_fire_blue: 0.25,
white_pin_fire_yellow: 0.23,
white_pin_fire_purple: 0.16,
white_pin_fire_pink: 0.15
}
pdf.values.sum
#=> 1.0
I've made the values floats rather than strings merely to avoid the need for a boring conversion. Note that the keys, which are symbols, do not require single quotes here.
We can assume that all of the values in pdf are positive, as any that are zero can be removed.
Let's first create a method that converts pdf (probability density function) to cdf (cumulative probability distribution).
def pdf_to_cdf(pdf)
cum = 0.0
pdf.each_with_object({}) do |(k,v),h|
cum += v
h[cum] = k
end
end
cdf = pdf_to_cdf(pdf)
#=> {0.21=>:white_pin_fire_green,
# 0.45999999999999996=>:white_pin_fire_blue,
# 0.69=>:white_pin_fire_yellow,
# 0.85=>:white_pin_fire_purple,
# 1.0=>:white_pin_fire_pink}
Yes, I've inverted the cdf by flipping the keys and values. That's not a problem, since all pdf values are positive, and it's more convenient this way, for reasons to be explained.
For convenience let's now create an array of cdf's keys.
cdf_keys = cdf.keys
#=> [0.21, 0.46, 0.69, 0.85, 1.0]
We sample a single probability-weighted value by generating a (pseudo-) random number p between 0.0 and 1.0 (e.g., p = rand #=> 0.793292984248818) and then determine the smallest index i for which
cdf_keys[i] >= p
Suppose p = 0.65. then
cum_prob = cdf_keys.find { |cum_prob| cum_prob >= 0.65 }
#=> 0.69
Note that, because cdf_keys is an increasing sequence the operation
cum_prob = cdf_keys.find { |cum_prob| cum_prob >= rand }
could be sped up by using Array#bsearch.
So we select
selection = cdf[cum_prob]
#=> :white_pin_fire_yellow
Note that the probability that rand will be between 0.46 and 0.69 equals 0.69 - 0.46 = 0.23, which, by construction, is the desired probability of selecting :white_pin_fire_yellow.
If we wish to sample additional values "with replacement", we simply generate additional random numbers between zero and one and repeat the above calculations.
If we wish to sample additional values "without replacement" (no repeated selections), we must first remove the element just drawn from the pdf. First, however, let's note the probability of selection:
selection_prob = pdf[selection]
#=> 0.23
Now delete selection from pdf.
pdf.delete(:white_pin_fire_yellow)
pdf
#=> {:white_pin_fire_green=>0.21,
# :white_pin_fire_blue=>0.25,
# :white_pin_fire_purple=>0.16,
# :white_pin_fire_pink=>0.15}
As pdf.values.sum #=> 0.77 we must normalize the values so they sum to 1.0. To do that we don't actually have to sum the values as that sum equals
adj = 1.0 - selection_prob
#=> 1.0 - 0.23 => 0.77
Now normalize the new pdf:
pdf.each_key { |k| pdf[k] = pdf[k]/adj }
#=> {:white_pin_fire_green=>0.2727272727272727,
# :white_pin_fire_blue=>0.3246753246753247,
# :white_pin_fire_purple=>0.20779220779220778,
# :white_pin_fire_pink=>0.1948051948051948}
pdf.values.sum
#=> 1.0
We now repeat the steps described above when selecting the first element at random (construct cdf, generate a random number between zero and one, and so on).

Issue with getting correct highest average speed

I try to do this task:codewars kata
Description:
In John's car the GPS records every s seconds the distance travelled
from an origin (distances are measured in an arbitrary but consistent
unit). For example, below is part of a record with s = 15:
x = [0.0, 0.19, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25] The
sections are:
0.0-0.19, 0.19-0.5, 0.5-0.75, 0.75-1.0, 1.0-1.25, 1.25-1.50, 1.5-1.75, 1.75-2.0, 2.0-2.25 We can calculate John's average hourly speed on every section and we get:
[45.6, 74.4, 60.0, 60.0, 60.0, 60.0, 60.0, 60.0, 60.0] Given s and x
the task is to return as an integer the floor of the maximum average
speed per hour obtained on the sections of x. If x length is less than
or equal to 1 return 0: the car didn't move.
Example:
with the above data your function gps(x, s)should return 74
My code:
def gps(s, x)
i = 0
speed = 0
max = 0
0 if x.length <= 1
while i < x.length - 2
speed = get_speed(x[i].to_f, x[i + 1].to_f, s)
max = speed if speed > max
i += 1
end
print max.floor
end
def get_speed(a, b, s)
((b - a).abs * ((60 / s) * 60))
end
Problem is with passing some tests.
Tests:
gps(20, [0.0, 0.23, 0.46, 0.69, 0.92, 1.15, 1.38, 1.61]) result: 41 - correct
gps(12, [0.0, 0.11, 0.22, 0.33, 0.44, 0.65, 1.08, 1.26, 1.68, 1.89, 2.1, 2.31, 2.52, 3.25]) result: 77 - incorrect, should be 219.
I don't have idea where I do wrong. Could someone give some hint to resolve problem?
#mcfinnigan's answer correctly identifies the immediate mistake in your code, but the real underlying cause is that you are not writing idiomatic Ruby. If you were writing idiomatic Ruby (instead of FORTRAN in Ruby syntax, as you are doing), then you would use iterators instead of manually fiddling with loop indices and the problem wouldn't even arise in the first place. Something like this:
def gps(interval, measurements)
compute_result(interval, measurements).tap(&method(:print))
end
private
def compute_result(interval, measurements)
return 0 if measurements.length <= 1
hourly_speed(max_distance(*distances(*measurements)), interval)
end
def distances(*measurements)
measurements.
each_cons(2). # iterate over all consecutive pairs
map {|a, b| b - a } # transform to list of distances travelled
end
def max_distance(*distances)
distances.max
end
def hourly_speed(distance, time_in_seconds)
seconds_per_hour = 60.0 * 60
(distance * seconds_per_hour / time_in_seconds).floor
end
As you see, there are no loops, no indices, no loop conditions, in fact, apart from the edgecase of an empty measurements array, there are no conditions at all, and so there are no conditions, indices, loops etc. to get wrong.
The problem is broken down into smaller subproblems that can be tested and debugged individually. Every method returns a value (instead of just printing to the console), which makes it possible to easily test it automatically (and also to reuse it in other methods).
while i < x.length - 2
This appears to be the issue. A classic off-by-one error; you are not considering the final element in your array.
Change your condition to
while i < x.length - 1
and your bug goes away.

Discrete probability distribution with a given maximum

I have to generate a random discrete probability distribution of n elements with a given maximum value.
I know how to create a classic one but I have no clues how to generate a random discrete probability distribution of n elements who respect max(distribution) = p. By that I mean that the highest probability of the distribution must be p in [0;1[.
Any idea ?
You could use a hit and miss approach (often used in probability simulations to randomly choose elements which satisfy certain constraints). The performance is acceptable unless np is too close to 1. Here is a Python implementation, which should be easy enough to translate to other languages:
from random import random
def makeDist(n,p):
#assumes p*n > 1
miss = True
while miss:
x = [p]
rest = [random() for i in range(1,n)]
total = sum(rest)
rest = [x*(1-p)/total for x in rest]
if all(x < p for x in rest):
x.extend(rest)
miss = False #we have a hit!
return x
Typical output:
>>> dist = makeDist(6,0.2)
>>> dist
[0.2, 0.08986510724051082, 0.18690143846768711, 0.19758176720598397, 0.19299989610231708, 0.13265179098350102]
>>> max(dist)
0.2
>>> sum(dist)
1.0
>>> makeDist(3,0.35)
[0.35, 0.31154704906869274, 0.33845295093130723]
>>>

How can I generate random numbers whose average follows a sine wave in Ruby?

I'm not a math guy, so I don't really know what I'm trying to do is called, but I'm sure there's a name for it. ;-)
I'm wanting to generate an array of random numbers in Ruby whose average at each element in the array follows a sine wave. What I mean by average at each element is the average at element n would be ary[0..n].inject(:+).to_f / (n + 1). So, if I loop from 0..n over the array of random numbers and generate the average like I described, I'd like the resulting values to follow a sine wave. I just don't know how to actually generate the random numbers in such a way...
# assuming `ary` is the array of random numbers
# I'm trying to figure out how to generate...
averages = []
(0..ary.size).each do |n|
averages << ary[0..n].inject(:+).to_f / (n + 1)
end
# `averages` should plot as a sine wave now...
Here's an idea. Create a class that has some sample size over which it generates points in a sine wave plus some random "fudge factor" (variance) above or below that point. This way, if you plot the number of points in the sample size you should see a sine wave with "roughness" according to the configured variance (fudge factor).
class RandomSineWave
attr_reader :size
def initialize(size=20, variance=0.2)
#size = size
#step = 2 * Math::PI / size
#position = 0
#variance = variance
end
def next
#position = 0 if #position >= 2 * Math::PI
next_rand = Math.sin(#position) + (rand * #variance) - (#variance / 2)
#position += #step
next_rand
end
end
# Generate TSV output for demonstration.
rsw = RandomSineWave.new
rsw.size.times { |i| puts [i, rsw.next].join "\t" }
You can fiddle with the "roughness" by modifying the second argument to the constructor:
rsw = RandomSineWave.new(20, 0.8) # Results plotted below...
As I understand, given some positive integer n, you want to construct an array of n probability distributions such that the expected values of partial sums of random variables describes a sine wave. I presume the sine wave is over the interval (0..2*π) and that the expected values are to be evenly spaced over that interval.
We must first ask if these probability distributions are statistically independent. If they are not, it becomes hopelessly complex, so I will assume they are independent. Whether they are identical distributions, after adjusting for differences in their means, is not necessary or even important. I'll come back to that later.
Since you want the expected values of partial sums of the random variables Xi to describe a sine wave, we require that:
E [∑j=0...iXj] = k * sin(2*π*i/n)
for all i = 0...n-1, for a given scale factor, k (with (E[..] denoting "expected value"). We can assume, without loss of generality, that k=1, as we can always scale the random variables by k, resulting in their means being scaled by the same constant.
Because the distributions are independent, we can write:
∑j=0...imj = sin(2*π*i/n)
where
mi = E[Xi] is Xi's mean.
In Ruby-speak, for an array x of n values (floats), this is:
x[0,i].reduce(:+) = Math::sin(2.0 * Math::PI * i.to_f/n)
We can easily compute x. Assume n = 36.
For i = 0:
x[0,0].reduce(:+) = Math::sin(2.0 * Math::PI * 0.0/36)
# x[0] = 0
Let:
s = x[0]
#=> 0.0
For i = 1:
x[0,1].reduce(:+) = Math::sin(2.0 * Math::PI * 1.0/36).round(6)
#=> 0.0 + x[1] = Math::sin(0.17453292519943295).round(6)
#=> = 0.173648
So
x[1] = 0.173648 - 0.0
#=> 0.173648
Now let
s += x[1]
#=> 0.173648
For i = 2:
x[0,2].reduce(:+) = Math::sin(2.0 * Math::PI * 2.0/36).round(6)
#=> s + x[2] = Math::sin(0.3490658503988659).round(6)
#=> 0.173648 + x[2] = 0.342020
So
x[2] = 0.342020 - 0.173648
#=> 0.168372
We then update s:
s += 0.168372
#=> 0.173648 += 0.168372
#=> 0.342020
and then compute x[3] similarly, then each of the remaining x's:
def compute(n, p=6)
sum = 0.0
n.times.map do |i|
if i.zero?
[0.0, 0.0, 0.0, 0.0]
else
x = Math::sin(2.0 * Math::PI * i.to_f/n) - sum
sum += x
[(2.0*(i.to_f/n)*Math::PI).round(p), x.round(p),
sum.round(p), Math::sin(sum).round(p)]
end
end
end
compute(36)
# radians x sum sin(sum) degrees
# [[0.0, 0.0, 0.0, 0.0 ], 0
# [0.174533, 0.173648, 0.173648, 0.172777],
# ...
# [1.396263, 0.045115, 0.984808, 0.833166],
# [1.570796, 0.015192, 1.0, 0.841471], 90
# [1.745329, -0.015192, 0.984808, 0.833166],
# ...
# [2.967060, -0.168372, 0.173648, 0.172777],
# [3.141593, -0.173648, 0.0, 0.0 ], 180
# [3.316126, -0.173648, -0.173648, -0.172777],
# ...
# [4.537856, -0.045115, -0.984808, -0.833166],
# [4.712389, -0.015192, -1.0, -0.841471], 270
# [4.886922, 0.015192, -0.984808, -0.833166],
# ...
# [5.934119, 0.15798, -0.34202, -0.335391],
# [6.108652, 0.168372, -0.173648, -0.172777]] 350
I will add a plot of these values when I have time to familiarize myself with #maeric's nifty plotting tool.
Now that we have the means, we can consider constructing probability distributions having those means.
Suppose, for example, we assume each random variable has the same uniform distribution with range (max-min) of rng, for varying means. If the mean were, say, 0.325467, we could generate a pseudo random-variate as follows:
rng * (rand-0.325467)
where
(0.5-0.174533).round(6)
#=> 0.325467
We therefore can generate pseudo-random variates for a uniform distribution with a given range and mean as follows:
def uniform_rv(rng, mean)
rng.to_f * (rand -0.5 -mean)
end

Resources