Continuous Proportional Transform1

Introduction

The ContinuousProportional transform adapts values from the driver domain to a bounded range, where the relative concentrations at each extreme of the range are specified, and the concentrations in between progress as an equal-ratios curve.

The range of values output by ContinuousProportional.convert() is controlled by two parameters implemented as Java fields: minRange and maxRange. These have the restriction that minRange < maxRange.

The distribution of values between minRange and maxRange is controlled by two additional parameters: origin and goal. The restrictions upon origin and goal are these:

Each ContinuousProportional instance internally maintains a ContinuousDistribution instance which divides the range from zero to unity into trapezoids of equal width. When goal > origin, the trapezoid height for sample value z is calculated using the formula:

origin * Math.pow(goal/originz)

When goal < origin the formula is:

goal * Math.pow(origin/goal, 1 - z)

The convert() method maps a value x in the driver domain from zero to unity into a value v in the application-range from minRange to maxRange in two steps. The first step uses ContinuousDistribution.quantile() to recast the driver value x into an intermediate value z, also between zero and unity. The second step applies the linear interpolation formula:

v = (maxRange-minRange)*z + minRange.

Profile

Figure 1 illustrates the influence which ContinuousProportional.convert() exerts over driver sequences. This panel was created using the same driver sources used for the ContinuousUniform, which earlier panel provides a basis for comparison.


Figure 1: Panel of ContinuousProportional output from three different Driver sources. The weight associated with the upper range bound is three times the weight associated with the lower range bound. Each row of graphs provides a time-series graph of samples (left) and a histogram analyzed from the same samples (right). The first row of graphs was generated using the standard random number generator. The second row was generated using the balanced-bit generator. The third row was generated using an ascending sequence of driver values, equally spaced from zero to unity.

The standard-random time-series graph (top row of Figure 1) has the same relative ups and downs as the standard-random time-series graph prepared for ContinuousUniform, but the specific values are squinched up toward the upper range bound. This difference becomes much clearer in the standard-random histogram, where the whitespace separating the vertical v axis from the smallest f(v) value progressively increases as v increases from zero to unity. Notice that while these histogram peaks and valleys are similar to those derived for ContinuousUniform, they are not the same. The fact that values squinch upwards means that range values which fell into the bottommost histogram region in the uniform histogram were spread across the bottom three regions here in the proportional histogram. Likewise the range values which fell into the topmost histogram region here were spread across three regions in the uniform histogram.

The balanced-bit time-series (middle row of Figure 1) likewise has the same ups and downs as the balanced-bit time-series graph prepared for ContinuousUniform with values squinched similarly. Since balanced-bit sequences strive aggressively for uniformity, the histogram peaks and valleys are comparatively restrained.

The time-series graph generated using ascending, equally spaced driver values (bottom row of Figure 1) presents the percentile function or CDF for this particular flavor of continuous proportional distribution. The histogram of sample values presents the distribution's probability density function or PDF. The PDF is an equal-ratios curve bending upward from f(v) = 1 when v = 0 to f(v) = 3 when v = 1. Looking back at the time-series graph, notice how the percentile function rises more steeply where the distribution is rarefied and less steeply where the distribution is concentrated.

For each graph in Figure 1 the average sample value is plotted as a dashed green line, while the interval between ± one standard deviation around the average is filled in with a lighter green background. For the ideally uniform driver values plotted in the third row of graphs, the average sample value is 0.590 and the standard deviation is 0.280. The interval from 0.590-0.280 to 0.590+0.0.280 is 2*0.280 = 0.56 = 56% of the full application range from zero to unity. Since the continuous uniform distribution had 58% of samples within ± one standard deviation of the mean, this suggests that with the proportional transform with origin 1 and goal 3 is squeezing 58% of samples into 56% of the application range, giving a concentration rate of 58/37 = 1.04.

Coding

/**
 * The {@link ContinuousProportional} class models a continuous distribution
 * over the range from {@link #minRange} to {@link #maxRange}.
 * {@link #origin} holds the density at {@link #minRange} while {@link #goal} holds the density at {@link #maxRange}.
 * Intervening densities are interpolated along a proportional (exponential) curve.
 * <p>
 * As a statistical transform, {@link ContinuousProportional} does not itself employ probability or randomness.
 * Instead it responds to an externally generated driver sequence which may or may not be random.
 * {@link ContinuousDistribution#quantile(double)} converts the driver value to an outcome.
 * </p>
 * <p>
 * For more information including a graph mapping driver values to results and a second graph showing a random population, see <a href="http://www.jstor.org/stable/1513123"><i>A Catalog of Statistical Distributions</i>, 1991</a>.
 * </p>
 * @author Charles Ames
 */
public class ContinuousProportional extends BoundedTransform {
   /**
    * Determines weight as of leftmost bound.
    */
   private double origin;
   /**
    * Determines weight as of rightmost bound.
    */
   private double goal;
   private int itemCount;
   /**
    * Determines whether a change in parameter values requires the distribution to be recalculated.
    */
   private boolean valid;
   /**
    * Constructor for {@link ContinuousProportional} instances.
    * @param container An entity which contains this transform.
    */
   public ContinuousProportional(WriteableEntity container) {
      super(container);
      this.origin = Double.NaN;
      this.goal = Double.NaN;
      this.itemCount = Integer.MIN_VALUE;
      this.valid = false;
   }
   /**
    * Getter for {@link origin}.
    * @return The assigned {@link origin} value.
    * @throws UninitializedException when {@link origin} is not initialized.
    */
   public double getOrigin() {
      if (Double.isNaN(this.origin)) throw new UninitializedException("Origin not initialized");
      return origin;
   }
   /**
    * Setter for {@link origin}.
    * @param origin The intended {@link origin} value.
    * @throws IllegalArgumentException when the origin is negative.
    */
   public void setOrigin(double origin) {
      checkOrigin(origin);
      if (this.origin != origin) {
         this.origin = origin;
         this.valid = false;
      }
   }
   /**
    * Check if the indicated value is suitable for {@link origin}.
    * @param origin The indicated value.
    */
   public void checkOrigin(double origin) {
      if (0. > origin)
         throw new IllegalArgumentException("Negative origin");
   }
   /**
    * Getter for {@link goal}.
    * @return The assigned {@link goal} value.
    */
   public double getGoal() {
      return goal;
   }
   /**
    * Setter for {@link goal}.
    * @param goal The intended {@link goal} value.
    * @throws IllegalArgumentException when the goal is negative.
    */
   public void setGoal(double goal) {
      checkGoal(goal);
      if (this.goal != goal) {
         this.goal = goal;
         this.valid = false;
      }
   }
   /**
    * Check if the indicated value is suitable for {@link goal}.
    * @param goal The indicated value.
    */
   public void checkGoal(double goal) {
      if (0. > goal)
         throw new IllegalArgumentException("Negative goal");
   }
   /**
    * Getter for {@link itemCount}.
    * @return The assigned {@link itemCount} value.
    */
   public int itemCount() {
      if (Integer.MIN_VALUE == this.itemCount) throw new UninitializedException("Item count not initialized");
      return this.itemCount;
   }
   /**
    * Setter for {@link itemCount}.
    * @param itemCount The intended {@link itemCount} value.
    */
   public void setItemCount(int itemCount) {
      checkItemCount(itemCount);
      if (this.itemCount != itemCount) {
         this.itemCount = itemCount;
         this.valid = false;
      }
   }
   /**
    * Check if the indicated value is suitable {@link itemCount}.
    * @param itemCount The indicated value.
    */
   public void checkItemCount(int itemCount) {
      if (10 > itemCount)
         throw new IllegalArgumentException("Grain too coarse");
   }
   @Override
   public Double convert(double driver) {
      if (!valid) {
         getDistribution().calculateProportional(getOrigin(), getGoal(), itemCount());
         valid = true;
      }
      return super.convert(driver);
   }
}
Listing 1: The ContinuousProportional implementation class.

The type hierarchy for ContinuousProportional is:

Class ContinuousProportional has superclass BoundedTransform, while class BoundedTransform in turn has superclass ContinuousDistributionTransform. Class ContinuousDistributionTransform embeds a ContinuousDistribution instance capable of approximating most any continuous distribution as a succession of trapezoids.

Each ContinuousDistribution trapezoid item has left, right, origin, and goal fields. Of the single item for ContinuousProportional, left is zero, right is unity, origin is set equal to ContinuousProportional.origin, and goal is set equal to ContinuousProportional.goal.

Notice that the trapezoid ranges from zero to unity, not minRange to maxRange. The trick with leveraging ContinuousDistribution instances is that the succession of trapezoids needs recalculating every time a parameter changes. Updating one single trapezoid item is not that big a deal, but more typically the number of will be 20 or more (my canned Normal distribution uses 200 trapezoids); also, the calculating formulas often include exponents. So it makes sense to abstract the range boundaries out of the distribution and to apply range scaling separately.

The distributing step of conversion happens in ContinuousDistributionTransform, where the convert() method does this:

return distribution.quantile(driver);

Range scaling happens in BoundedTransform, where the convert() method does this:

return interpolate(super.convert(driver));

And BoundedTransform.interpolate(factor) does this (ignoring pesky initialization checks):

return (maxRange-minRange)*factor + minRange;.

ContinuousProportional has a valid field to flag parameter changes. This field starts out false and reverts to false with every change to either origin or goal. Each call to ContinuousProportional.convert() begins by testing valid. If a parameter change has rendered the distribution invalid, convert() calls on the distribution to regenerate its single trapezoid item

Comments

  1. The present text is adapted from my Leonardo Music Journal article from 1991, "A Catalog of Statistical Distributions". The heading is "Proportional", p. 63.

© Charles Ames Page created: 2022-08-29 Last updated: 2022-08-29