Continuous Gamma Transform1

Introduction

The ContinuousGamma transform adapts values from the driver domain to a bounded range, where the relative concentrations are governed by a member of the gamma family of distribution curves.

The range of values output by ContinuousGamma.convert() is bounded below by zero and unbounded above. A practical upper bound is indirectly controlled by extent, a positive integer maintained as a Java field. The upper bound is calculated by multiplying the extent by the standard deviation.

The shape of the distribution curve is controlled by two parameters: shape, symbolized k and scale, symbolized λ. Both shape parameters must be greater than zero.

Each ContinuousGamma instance internally maintains a ContinuousDistribution instance which divides the range from zero to this calculated upper bound into trapezoids of equal width. The number of trapezoids is determined by a fourth parameter maintained as a Java field, itemCount. For the present purposes I have set itemCount to 200.

The trapezoid height for sample value x is calculated using the formula:

Math.pow(xshape - 1) * Math.pow(-x / scale)

For the gamma family of distribution curves, Wikipedia gives a parametric mean (average, symbolized μ) of kλ and a parametric variance (squared deviation, symbolized σ2) of kλ2. The upper bound thus calculates out to:

maxRange = extent * Math.sqrt(shape) * scale

Since minRange is fixed at zero, The width of each ContinuousDistribution trapezoid is here maxRange/itemCount.

Profile

Figure 1 illustrates the influence which ContinuousGamma.convert() exerts over driver sequences when the shape k = 2 and the scale λ = 5. 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 ContinuousGamma output from three different Driver sources. 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 down toward zero. Its hard to see how the driver distribution influences the standard-random histogram presented here, other than the generally ragged shape of the 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 jaggedness of this balanced-bit histogram is accodingly moderated.

The time-series graph generated using ascending, equally spaced driver values (bottom row of Figure 1) presents the quantile function for this flavor of continuous gamma 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 quantile 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 k = 2 and λ = 5 the parametric average calculates out to μ = 2×5 = 10, the parametric standard deviation calculates out to σ = √2×5 = √50 = 7.07, and the upper bound calculates out to 6×7.07 = 42.43. By contrast the numerical average and deviations for the bottom row of graphs were 10.135 and 6.854. Since this bottommost row illustrates the most ideal conditions under which a profile can be generated, these parametric and numerical statistics should match closely. I was able to narrow the gap between parametric and numerical averages by doubling the itemCount, however this change did nothing to close the gap between parametric and numerical deviations. I am hoping that the deviation gap will close with increasing sample counts.

The interval from 10.135-0.280 to 10.135+6.854 is 2*6.854 = 13.708 = 32% 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 gamma distribution with shape 2 and scale 5 is squeezing 58% of samples into 32% of the application range, giving a concentration rate of 58/32 = 1.81.

The Gamma Family of Probability Curves

Figures 2 (a) through 2 (c) show how changes in parameter settings affect the distribution curves. Each figure provides two graphs. The upper graph shows the probability density function or PDF. The lower graph shows the cumulative distribution function or CDF.


 λ = 0.5,  λ = 1.0,  λ = 1.5,  λ = 2.5,  λ = 4.0
Figure 2 (a): Gamma distribution curves for shape parameter k = 1.

When the shape parameter k = 1 as in Figure 2 (a), the gamma distribution simplifies to the exponential distribution that characterizes waiting durations in the Poisson Point Process.


 λ = 0.5,  λ = 1.0,  λ = 1.5,  λ = 2.5,  λ = 4.0
Figure 2 (b): Gamma distribution curves for shape parameter k = 2.

For shape k = 2 as in Figure 2 (b) the distribution characterizes what happens when two consecutive Poisson-Point waiting durations are added together.


 λ = 0.5,  λ = 1.0,  λ = 1.5,  λ = 2.5,  λ = 4.0
Figure 2 (c): Gamma distribution curves for shape parameter k = 4.

For shape k = 4 as in Figure 2 (c) the distribution characterizes what happens when four consecutive Poisson-Point waiting durations are added together.

Coding

/**
 * The {@link ContinuousGamma} class maps driver values to the range from
 * zero upwards.
 * A chi-square distribution happens when {@link #shape} is r/2 and
 * {@link #scale} is 2.
 * A negative exponential distribution happens when {@link #shape} is 1
 * and {@link #scale} is 1/lambda.
 * @author Charles Ames
 */
public class ContinuousGamma extends ContinuousDistributionTransform {
   /*
    * The shape parameter; controls the initial hump.
    */
   private double shape;
   /*
    * The scale parameter; controls exponential roll-off.
    */
   private double scale;
   /**
    * Determines the range extent in standard deviations.
    */
   private int extent;
   /**
    * Controls how many trapezoidal segments are used to approximate
    * the distribution.
    */
   private int itemCount;
   /**
    * Constructor for {@link ContinuousGamma} instances.
    * @param container An entity which contains this transform.
    */
   public ContinuousGamma(WriteableEntity container) {
      super(container);
      this.shape = Double.NaN;
      this.scale = Double.NaN;
      this.extent = 6;
      this.itemCount = Integer.MIN_VALUE;
   }
   /**
    * Getter for {@link #shape} .
    * @return The assigned {@link #shape} value.
    * @throws UninitializedException when {@link #shape} has not
    * been initialized.
    */
   public double getShape() {
      if (Double.isNaN(shape))
         throw new UninitializedException("Shape not initialized");
      return shape;
   }
   /**
    * Setter for {@link #shape}.
    * Distribution is recalculated.
    * @param shape The intended {@link #shape} value.
    * @throws IllegalArgumentException when the argument is not positive.
    */
   public void setShape(double shape) {
      checkShape(shape);
      if (this.shape != shape) {
         this.shape = shape;
         invalidate();
      }
   }
   /**
    * Check if the indicated value is suitable for {@link #shape}.
    * @param shape The indicated value.
    * @throws IllegalArgumentException when the argument is not positive.
    */
   public void checkShape(double shape) {
      if (MathMethods.TINY > shape)
         throw new IllegalArgumentException("Shape must be positive");
   }
   /**
    * Getter for {@link #scale}.
    * @return The assigned {@link #scale} value.
    * @throws UninitializedException when {@link #scale} is not initialized.
    */
   public double getScale() {
      if (Double.isNaN(scale))
         throw new UninitializedException("Scale not initialized");
      return scale;
   }
   /**
    * Setter for {@link #scale}.
    * Distribution is recalculated.
    * @param scale The intended {@link #scale} value.
    * @throws IllegalArgumentException when the argument is not positive.
    */
   public void setScale(double scale) {
      checkScale(scale);
      if (this.scale != scale) {
         this.scale = scale;
         invalidate();
      }
   }
   /**
    * Check if the indicated value is suitable for {@link #scale}.
    * @param scale The indicated value.
    * @throws IllegalArgumentException when the argument is not positive.
    */
   public void checkScale(double scale) {
      if (MathMethods.TINY > scale)
         throw new IllegalArgumentException("Scale must be positive");
   }
   /**
    * Setter for {@link extent}.
    * Distribution is recalculated.
    * @param extent The intended {@link extent} value.
    */
   public void setExtent(int extent) {
      checkExtent(extent);
      if (this.extent != extent) {
         this.extent = extent;
         invalidate();
      }
   }
   /**
    * Getter for {@link extent} .
    * @return The assigned {@link extent} value.
    * @throws UninitializedException when {@link extent} has not
    * been initialized.
    */
   public int getExtent() {
      if (Integer.MIN_VALUE == extent)
         throw new UninitializedException("Extent not initialized");
      return extent;
   }
   /**
    * Check if the indicated value is suitable for {@link extent}.
    * @param extent The indicated value.
    */
   public void checkExtent(int extent) {
      if (0 >= extent)
         throw new IllegalArgumentException("Extent not positive");
   }
   /**
    * Getter for {@link #itemCount}.
    * @return The assigned {@link #itemCount} value.
    * @throws UninitializedException when {@link itemCount} is
    * not initialized.
    */
   public int itemCount() {
      if (Integer.MIN_VALUE == itemCount)
         throw new UninitializedException("Item count not initialized");
      return itemCount;
   }
   /**
    * Setter for {@link #itemCount}.
    * Distribution is recalculated.
    * @param itemCount The intended {@link #itemCount} value.
    */
   public void setItemCount(int itemCount) {
      checkItemCount(itemCount);
      if (this.itemCount != itemCount) {
         this.itemCount = itemCount;
         invalidate();
      }
   }
   /**
    * Check if the indicated value is suitable for {@link #itemCount}.
    * @param itemCount The indicated value.
    */
   public void checkItemCount(int itemCount) {
      if (itemCount < 8)
         throw new IllegalArgumentException("Item count too small");
   }
   @Override
   protected void validate(DistributionBase<Double> distribution) {
      ((ContinuousDistribution) distribution).calculateGamma(
            getShape(), getScale(), getExtent(), itemCount());
   }
   @Override
   public Double minGraphValue(double tail) {
      return 0.0;
   }
   @Override
   public Double minRange() {
      return 0.0;
   }
}
Listing 1: The ContinuousGamma implementation class.

The type hierarchy for ContinuousGamma is:

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.

Conversion happens entirely in ContinuousDistributionTransform, where the convert() method does this:

return getDistribution().quantile(driver);

TransformBase maintains a valid field to flag parameter changes. This field starts out false and reverts to false with every time ContinuousGamma calls TransformBase.invalidate(). This happens with any change to shape, scale, extent or itemCount. Any call to TransformBase.getDistribution() (and ContinuousDistributionTransform.convert() makes such a call) first creates the distribution if it does not already exist, then checks valid. If false, then getDistribution() calls validate(), which is abstract to TransformBase but whose implementation is made concrete by ContinuousGamma. And that particular implementation of validate() makes use of ContinuousDistribution.calculateGamma(shape, scale, extent, itemCount) to recalculate the succession of trapezoids.

Comments

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

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