2

I have a method called "GetValue()" which is supposed to return the value "A", "B", "C" or "D" on each method call.

I want this method to return the value "A" in 30% of the method calls and the value "B" in 14% of the method calls, the value "C" 31%.. and so on...

Wich is the best way to distribute theese values smoothly, I do not want the method to return the value "A" xxx times in a row becouse the value "A" are farest from it's requested outcome percentage.

Please, all answeres are appreciated.

tost
  • 43
  • 5

6 Answers6

9

You can use the Random class to achieve this:

private static Random Generator = new Random();

public string GetValue() 
{
  var next = Generator.Next(100);
  if (next < 30) return "A";
  if (next < 44) return "B";
  if (next < 75) return "C";
  return "D";
}

Update

For a more generic random weighted value store, the following may be a good starting point:

public class WeightedValueStore<T> : IDisposable
{
  private static readonly Random Generator = new Random();

  private readonly List<Tuple<int, T>> _values = new List<Tuple<int, T>>();
  private readonly ReaderWriterLockSlim _valueLock = new ReaderWriterLockSlim();

  public void AddValue(int weight, T value)
  {
    _valueLock.EnterWriteLock();
    try 
    {
      _values.Add(Tuple.Create(weight, value));
    }
    finally
    {
      _valueLock.ExitWriteLock();
    }
  }      

  public T GetValue() 
  {
    _valueLock.EnterReadLock();
    try
    {
      var totalWeight = _values.Sum(t => t.Item1);
      var next = Random.Next(totalWeight);
      foreach (var tuple in _values)
      {
        next -= tuple.Item1;
        if (next < 0) return tuple.Item2;
      }
      return default(T); // Or throw exception here - only reachable if _values has no elements.
    }
    finally
    {
      _valueLock.ExitReadLock();
    }
  }

  public void Dispose()
  {
    _valueLock.Dispose();
  }
}

Which would then be useable like so:

public string GetValue() 
{
  using (var valueStore = new WeightedValueStore<string>()) 
  {
    valueStore.AddValue(30, "A");
    valueStore.AddValue(14, "B");
    valueStore.AddValue(31, "C");
    valueStore.AddValue(25, "D");
    return valueStore.GetValue();
  }
}
Rich O'Kelly
  • 41,274
  • 9
  • 83
  • 114
  • 1
    Though if one is new to `Random` it can be tricky to get right. http://csharpindepth.com/Articles/Chapter12/Random.aspx – Oded Jan 03 '12 at 13:24
  • Random won't work if he wants the results to follow an approximate 'hit' rate of 30%, 14%, 31% and so on (?) - Don't have a solution but Random isn't the answer to the problem – K. Bob Jan 03 '12 at 13:26
  • 4
    @K.Bob Yes it is - Random generates numbers that are normally distributed. – Rich O'Kelly Jan 03 '12 at 13:27
  • @K.Bob : would you care to elaborate? It seems somewhat worrying that `Random` wouldn't be a good fit for this. – spender Jan 03 '12 at 13:29
  • OK - the additional information makes sense. My initial thought was that you meant just to pull ABCD randomly - sorry. – K. Bob Jan 03 '12 at 13:29
  • But it still doesn't answer his problem of not wanting A xxx number of times in a row (xxx presumably meaning more than once) – K. Bob Jan 03 '12 at 13:36
  • Maybe it's out of the scope of this question, but I believe this induces bad coding habits for a beginner. If he needs to handle another possible value (`E`), he'll find himself having to rewrite all these `ifs` (not to mention handling a set of data unknown at compile time, which is not an uncommon scenario). – Konrad Morawski Jan 03 '12 at 13:57
  • @K.Bob: that's the trouble with random numbers. Sometimes they just don't feel random _enough_. :) – sarnold Feb 18 '12 at 03:04
3

Use Random. Take care of the seed. See this link.

Example:

    // You can provide a seed as a parameter of the Random() class.
    private static Random RandomGenerator = new Random();

    private static string Generate()
    {
        int value = RandomGenerator.Next(100);

        if (value < 30)
        {
            return "A";
        }
        else if (value < 44)
        {
            return "B";
        }
        else
        {
            return "C";
        }
    }
Community
  • 1
  • 1
ken2k
  • 48,145
  • 10
  • 116
  • 176
  • Why doubles? `Random` can generate ints. Why not use this? – spender Jan 03 '12 at 13:30
  • If you seed the random generator with a contant value, it will always generate the same sequence, so it's not random at all. – Guffa Jan 03 '12 at 13:45
  • @Guffa: I've used a constant to note that there is an extra constructor that takes a seed as parameter. I've provided a link with details about the seed. You can use a value based on the timestamp for instance. BTW, the most important thing in the example is that the the Random instance is generated once (static), so multiple calls to the Generate() method will return different values. – ken2k Jan 03 '12 at 13:49
  • Actually you're right this is confusing. I'll remove the seed value in the example. – ken2k Jan 03 '12 at 13:51
2

If you want that distribution by average, you can just pick a random number and check it.

Random rnd = new Random();

int value = rnd.Next(100); // get a number in the range 0 - 99
if (value < 30) return "A";
if (value < 30+14) return "B";
if (value < 30+14+31) return "C";
return "D";

Note that you should create the random generator once, and reuse it for subsequent calls. If you create a new one each time, they will be initialised with the same random sequence if two method calls come too close in time.

If you want exactly that distribution for 100 items, then you would create an array with 100 items, where 30 are "A", 14 are "B", and so on. Shuffle the array (look up Fisher-Yates), and return one item from the array for each method call.

Guffa
  • 687,336
  • 108
  • 737
  • 1,005
2

Let's say you have the arrays
String[] possibleOutcomes = new String[] { "A", "B", "C", "D" }
and
int[] possibleOutcomeProbabilities = new int[] { 30, 14, 31, 25 }

You can use the following strategy whenever you are required to output one of the outcomes:

  1. Find the sum of all elements in possibleOutcomeProbabilities. Lets call this sum totalProbability.
  2. Generate a random number between 1 and totalProbability. Lets call this randomly generated number outcomeBucket.
  3. Iterate over possibleOutcomeProbabilities to determine which outcome outcomeBucket corresponds to. You then pick the corresponding outcome from possibleOutcomes.

This strategy will certainly not give you first 30% outcomes as A, next 14% as B, etc. However, as probability works, over a sufficiently large number of outcomes, this strategy will ensure that your possible outcomes are distributed as per their expected probabilities. This strategy gives you the advantage that outcome probabilities are not required to add up to 100%. You can even specify relative probabilities, such as, 1:2:3:4, etc.

If you are really worried about the fastest possible implementation for the strategy, you can tweak it as follows:

a. Calculate totalProbability only once, or when the probablities are changed. b. Before calculating totalProbability, see if the elements in possibleOutcomeProbabilities have any common divisors and eliminate those. This will give you a smaller probability space to traverse each time.

manish
  • 19,695
  • 5
  • 67
  • 91
1

try this:

Random r = new Random();

private string GetValue()
{
  double d = r.Next();
  if(d < 0.3)
    return "A";
  else if(d < 0.5)
    return "B";

  ...etc.

}

EDIT: just make sure that the Random variable is created outside the function or you'll get the same value each time.

LoveMeSomeCode
  • 3,888
  • 8
  • 34
  • 48
  • it returns a number. of some sort. it really doesn't matter, I wasn't trying to write it for him, I was trying to point him in the right direction, and except for a possible cast, the code is correct. – LoveMeSomeCode Jan 03 '12 at 15:51
  • again, that's ok. He's got the documentation. He doesn't need someone to hold his hand and pick out every overload and every parameter for him. He can type in the code and see that there are Next methods on that class for doubles, ints, etc. If I were struggling with this concept and someone gave me this solution it would close 99% of the gap and I'd have what I needed. I don't really look at this site as a place for people to write my code for me. I see all code samples as pseudo-code to point people in the right direction. – LoveMeSomeCode Jan 05 '12 at 13:42
1

I would not recommend any hard-coded approach (it is hard to maintain and it's bad practice). I'd prefer a more generic solution instead.

    enum PossibleOutcome { A, B, C, D, Undefined }

    // sample data: possible outcome vs its probability
    static readonly Dictionary<PossibleOutcome, double> probabilities = new Dictionary<PossibleOutcome, double>()
    {
        {PossibleOutcome.A, 0.31},
        {PossibleOutcome.B, 0.14},
        {PossibleOutcome.C, 0.30},
        {PossibleOutcome.D, 0.25}
    };

    static Random random = new Random();       
    static PossibleOutcome GetValue()
    {            
        var result = random.NextDouble();
        var sum = 0.0;
        foreach (var probability in probabilities)
        {
            sum += probability.Value;
            if (result <= sum)
            {
                return probability.Key;
            }                
        }
        return PossibleOutcome.Undefined; // it shouldn't happen
    }

    static void Main(string[] args)
    {
        if (probabilities.Sum(pair => pair.Value) != 1.0)
        {
            throw new ApplicationException("Probabilities must add up to 100%!");
        }

        for (var i = 0; i < 100; i++)
        {
            Console.WriteLine(GetValue().ToString());
        }
        Console.ReadLine();            
    }
Konrad Morawski
  • 8,307
  • 7
  • 53
  • 91
  • Interresting, but perhaps overly complicated... Anyway, you might want to use integer numbers for the probabilities, if you add a bunch of floating point values you are likely to end up with something that is not exactly 1.0. – Guffa Jan 03 '12 at 14:20