4

I'm trying to create a random float generator (range of 0.0-1.0), where I can supply a single target value, and a strength value that increases or decreases the chance that this target will be hit. For example, if my target is 0.7, and I have a high strength value, I would expect the function to return mostly values around 0.7.

Put another way, I want a function that, when run a lot of times, would produce a distribution graph something like this:

Histogram

Something like a bell curve, yes, but with a strict range limit (instead of the -inf/+inf range limit of a normal distribution). Clamping a normal distribution is not ideal, I want the distribution to naturally end at the range limits.

The approach I've been attempting is to come up with a formula to transform a value from uniform distribution to the mythical distribution I'm envisioning. Something like an inverse sine:

Inverse Sine

with the ability to widen out that middle point, via the strength value:

Widened Midpoint

and also the ability to move that midpoint up and down, via the target value:

Target changed to 0.7 (courtesy of MS Paint because I couldn't figure this part out mathematically)

The range of this theoretical "strength value" is up for debate. I could imagine either a limited value, say between 0 and 1, where 0 means it's uniform distribution and 1 means it's a 100% chance of hitting the target; or, I could imagine a value that approaches a 100% chance the higher it gets, without ever reaching it. Something along either line would work.

I'm working in C# but this can be language-agnostic. Any help pointing me in the right direction is appreciated. Also happy to clarify further.

Sayse
  • 42,633
  • 14
  • 77
  • 146

2 Answers2

1

I'm not a mathematician but I took a look and I feel like I got something that might work for you.

All i did was take the normal distribution formula: enter image description here and use 0.7 as mu to shift the distribution towards 0.7. I added a leading coefficient of 0.623 to shift the values to be between 0 and 1 and migrated it from formula to C#, this can be found below.

enter image description here

Usage:

DistributedRandom random = new DistributedRandom();

// roll for the chance to hit
double roll = random.NextDouble();

// add a strength modifier to lower or strengthen the roll based on level or something
double actualRoll = 0.7d * roll;

Definition

public class DistributedRandom : Random
{
    public double Mean { get; set; } = 0.7d;

    private const double limit = 0.623d;
    private const double alpha = 0.25d;
    private readonly double sqrtOf2Pi;
    private readonly double leadingCoefficient;

    public DistributedRandom()
    {
        sqrtOf2Pi = Math.Sqrt(2 * Math.PI);
        leadingCoefficient = 1d / (alpha * sqrtOf2Pi);
        leadingCoefficient *= limit;
    }

    public override double NextDouble()
    {
        double x = base.NextDouble();

        double exponent = -0.5d * Math.Pow((x - Mean) / alpha, 2d);

        double result = leadingCoefficient * Math.Pow(Math.E,exponent);

        return result;
    }
}

Edit: In case you're not looking for output similar to the distribution histogram that you provided and instead want something more similar to the sigmoid function you drew I have created an alternate version.

Thanks to Ruzihm for pointing this out.

I went ahead and used the CDF for the normal distribution: enter image description here where erf is defined as the error function: enter image description here. I added a coefficient of 1.77 to scale the output to keep it within 0d - 1d.

It should produce numbers similar to this: enter image description here

Here you can find the alternate class:

public class DistributedRandom : Random
{
    public double Mean { get; set; } = 0.7d;

    private const double xOffset = 1d;
    private const double yOffset = 0.88d;
    private const double alpha = 0.25d;
    private readonly double sqrtOf2Pi = Math.Sqrt(2 * Math.PI);
    private readonly double leadingCoefficient;
    private const double cdfLimit = 1.77d;
    private readonly double sqrt2 = Math.Sqrt(2);
    private readonly double sqrtPi = Math.Sqrt(Math.PI);
    private readonly double errorFunctionCoefficient;
    private readonly double cdfDivisor;

    public DistributedRandom()
    {
        leadingCoefficient = 1d / (alpha * sqrtOf2Pi);
        errorFunctionCoefficient = 2d / sqrtPi;
        cdfDivisor = alpha * sqrt2;
    }

    public override double NextDouble()
    {
        double x = base.NextDouble();

        return CDF(x) - yOffset;
    }

    private double DistributionFunction(double x)
    {
        double exponent = -0.5d * Math.Pow((x - Mean) / alpha, 2d);

        double result = leadingCoefficient * Math.Pow(Math.E, exponent);

        return result;
    }

    private double ErrorFunction(double x)
    {
        return errorFunctionCoefficient * Math.Pow(Math.E,-Math.Pow(x,2));
    }

    private double CDF(double x)
    {
        x = DistributionFunction(x + xOffset)/cdfDivisor;

        double result = 0.5d * (1 + ErrorFunction(x));

        return cdfLimit * result;
    }
}
DekuDesu
  • 2,224
  • 1
  • 5
  • 19
  • Your formula shows a shape similar to the PDF of what asker's looking for, but you'll probably want to feed in the output of NextDouble into the inverse* of the integral of that function (which would be more like the CDF), and you'll probably need to scale that output to get a range between 0 and 1 as well. [this post](https://stackoverflow.com/q/35148658/1092820) has an explanation of that – Ruzihm Dec 14 '21 at 08:22
  • I came up with `1.77d * 0.5d * (1+erf((x-0.7d)/(0.25d * sqrt(2))))-1` which, Im going to be honest to say.. it looks very similar. I'm not sure if the extra CPU cycles would be worth it to calculate the CDF for a similar graph. Although, I'm going to be completely transparent I base this solely on the wiki for normal distribution and my programming experience, not based on my mathematics experience. Perhaps I did it wrong though. @Ruzihm – DekuDesu Dec 14 '21 at 08:40
  • @Ruzihm Nevermind, I figured out what you mean, I'll update my post with it. I'm literally braindead – DekuDesu Dec 14 '21 at 09:03
  • Nice! But it should be the inverse of that, it should look more like the inverse sin shown in the question, where it is at its most horizontal around y=mean. – Ruzihm Dec 14 '21 at 15:12
  • 2
    Wow I really appreciate the effort here. I think @Ruzihm is right, I'm looking for something like the inverse of your second formula here. I went down a similar path that you did at first, trying to create the formula for the final histogram, before realizing that wasn't exactly what I was looking for. And I'm fine with examining the normal distribution for all this, but my main problem with it is the symmetrical shape of it. I'm looking for a final distribution graph where 100% of the values fall within 0 and 1, but the "hump" of the curve slides around based on target, if that makes sense. – Jason Ericson Dec 14 '21 at 19:17
  • correction to my earlier comment - most horizontal around y=mode and has its median at (0.5, median value) – Ruzihm Dec 15 '21 at 17:13
1

I came up with a workable solution. This isn't quite as elegant as I was aiming for because it requires 2 random numbers per result, but it definitely fulfills the requirement. Basically it takes one random number, uses another random number that's exponentially curved towards 1, and lerps towards the target.

I wrote it out in python because it was easier for me to visualize the histogram of it:

import math
import random

# Linearly interpolate between a and b by t.
def lerp(a, b, t):
    return ((1.0 - t) * a) + (t * b)

# What we want the median value to be.
target = 0.7
# How often we will hit that median value. (0 = uniform distribution, higher = greater chance of hitting median)
strength = 1.0

values = []
for i in range(0, 1000):
    # Start with a base float between 0 and 1.
    base = random.random()

    # Get another float between 0 and 1, that trends towards 1 with a higher strength value.
    adjust = random.random()
    adjust = 1.0 - math.pow(1.0 - adjust, strength)

    # Lerp the base float towards the target by the adjust amount.
    value = lerp(base, target, adjust)

    values.append(value)

# Graph histogram
import matplotlib.pyplot as plt
import scipy.special as sps
count, bins, ignored = plt.hist(values, 50, density=True)
plt.show()

Target = 0.7, Strength = 1

Target = 0.7, Strength = 1

Target = 0.2, Strength = 1

Target = 0.2, Strength = 1

Target = 0.7, Strength = 3

Target = 0.7, Strength = 3

Target = 0.7, Strength = 0

(This is meant to be uniform distribution - it might look kinda jagged, but I tested and that's just python's random number generator.) Target = 0.7, Strength = 0

  • I think this is a pretty good result. But currently there is no guarantee that half the base results will be on the left vs the right of the target, so it it is not truly a median. With a small change, you can get that behavior: `if base < .5: base = base/.5 * target else: base = target + (base-.5)/.5 * (1-target)` https://replit.com/@Ruzihm/ProbDist This results in different behavior if strength=0. – Ruzihm Dec 15 '21 at 22:02
  • That being said, asker's histogram doesn't show that the mode is also the median, so I think this answer is perfectly acceptable as-is. I think including it as an option could improve the answer for future visitors who might want that behavior – Ruzihm Dec 15 '21 at 22:08
  • Correction to my above comment *expectation, not guarantee – Ruzihm Dec 15 '21 at 22:15