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 between 0 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.