Creating random assignment operators
New random operators can be created by extending the PlanOutOpRandom
class,
and implementing a simpleExecute()
and options()
method.
This class is the workhorse of PlanOut’s random assignment capabilities.
Random assignments are done primarily through two built-in helper methods that come with PlanOutOpRandom
:
getHash()
gets a random integer between0
and a very large number (0xFFFFFFFFFFFFFFF
), inclusive. Many random assignment procedures will mod the integer returned by this method to perform some type of random assignment.getUniform()
gets a floating point number between some range of values. This number should be uniform across the specified minimum and maximum value (default values are 0.0 and 1.0).
Hashes and numbers generated by these methods use the experiment-level,
variable-level, and unit-level salts, as described in the
How PlanOut Works guide.
They may also take optional argument, appended_unit
, which can
either be a scalar (e.g., integer or string) or list value
(consisting of scalars). This appended unit may be used to generate
multiple independent random numbers.
Examples
We use examples to show how implementing random operators works in practice.
UniformChoice
UniformChoice picks an element from a given input list by generating a random integer and modding it by the appropriate range.
class UniformChoice(PlanOutOpRandom):
def options(self):
return {'choices': {'required': 1, 'description': 'elements to draw from'}}
def simpleExecute(self):
choices = self.args.get('choices')
if len(choices) == 0:
return []
rand_index = self.getHash() % len(choices)
return choices[rand_index]
Here, we can see that the operator has one required argument, choices
. The simpleExecute()
method grabs this input parameter from self.parameters
, and then generates a random index by calling self.getHash()
.
RandomFloat
RandomFloat generates a random floating point number between two values.
class RandomFloat(PlanOutOpRandom):
def options(self):
return {
'min': {'required': 0, 'description': 'min (float) value drawn'},
'max': {'required': 0, 'description': 'max (float) value being drawn'}}
def simpleExecute(self):
min_val = self.args.get('min', 0)
max_val = self.args.get('max', 1)
return self.getUniform(min_val, max_val)
RandomFloat
takes two optional parameters, ‘min’ and ‘max’, which it passes into self.getUniform
to generate a uniform random number.
Generating normally distributed numbers
So far we have only looked at examples where random assignment is done by generating a single random number. Many operations, however, involve drawing multiple random numbers. For example, normally distributed numbers can be generated in a pseudo-random fashion using two independent uniformly distributed random numbers:
class Normal(PlanOutOpRandom):
def options(self):
return {
'mean': {'required': 1, 'description': 'mean value drawn'},
'sd': {'required': 1, 'description': 'standard deviation of normal'}}
def options():
def simpleEvaluate():
mean = self.args.get('mean')
sd = self.args.get('sd')
# Use the Box-Muller transform to generate a normal from two
# independent draws from a Uniform[0,1] distribution
# see http://en.wikipedia.org/wiki/Box-Muller_transform
theta = 2.0 * math.pi * self.getUniform(0.0, 1.0, 'theta')
rho = sqrt(-2.0 * log(1.0 - self.getUniform(0.0, 1.0, 'rho')))
return mean + sd * rho * math.cos(theta)
In the code above, we use the optional appended_unit parameter (the third parameter to getUniform()
) to make multiple draws from the uniform distribution to generate a normally distributed floating point number, using the Box-Muller transform.