Sunday, October 12, 2014

WeightedRandom - Select content based on weights (C# / .NET)

Need to select content from a collection based on weights?  Here is a great .NET 4+ class that is very helpful.

The class can be used with just about any collection and you can pass in a delegate/lamda to determine what the weight is on each collection object.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
//
// THIS IS ALL JUST TEST CODE
//
// THE ACTUAL WEIGHTED RANDOM CODE IS IN THE WeightedRandom.cs file
//
namespace PaulTechGuy.Weights
{
class Program
{
class Foo
{
public string Name { get; set; }
public int Weight { get; set; }
public Foo(string name, int weight)
{
Name = name;
Weight = weight;
}
}
static void Main(string[] args)
{
new Program().Run(args);
}
private void Run(string[] args)
{
// test data
Foo[] items = new Foo[]
{
// there are six individual weights from 10 to 60)
new Foo("Sally Target=17% (1/6th)", 10),
new Foo("Fred Target=30% (1/3th)", 30), // or 2/6
new Foo("John Target=50% (1/2th)", 60), // or 3/6
};
// here is the magic; declare your collection Func weight delegate and
// create the WeightedRandom object; after that you just call Next() to
// get your next random object based on weights
Func<Foo, int> fweight = f => f.Weight;
WeightedRandom<Foo> foos = new WeightedRandom<Foo>(items, fweight, new Random());
// spit out a bunch of test results
for (int times = 0; times < 5; ++times)
{
// generate a bunch of random test data and store counts
Dictionary<string, int> statistics = new Dictionary<string, int>();
for (int i = 0; i < 1000; ++i)
{
Foo foo = foos.Next();
if (!statistics.ContainsKey(foo.Name))
{
statistics.Add(foo.Name, 0);
}
statistics[foo.Name] += 1;
}
// print out results
double total = statistics.Sum(s => s.Value);
foreach (var kvp in statistics)
{
Console.WriteLine("{0} => {1} (Actual {2}%)", kvp.Key, kvp.Value, Math.Round(kvp.Value / total * 100));
}
Console.WriteLine();
}
}
}
}
view raw Program.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
namespace PaulTechGuy.Weights
{
public class WeightedRandom<T>
{
private readonly List<IGrouping<int, T>> _itemMap;
private readonly Random _random;
private readonly int _maxWeight;
public WeightedRandom(T[] items, Func<T, int> weight, Random rnd)
{
Contract.Requires(items.Length > 0);
_random = rnd;
// map the items by weight and sort by weight
_itemMap = items
.GroupBy(weight)
.OrderBy(g => weight)
.ToList();
// weights are sorted in ascending order; max weight will be last element
_maxWeight = _itemMap.ElementAt(_itemMap.Count - 1).Key;
}
public T Next()
{
// select a random weight
int randomWeight = _random.Next(_maxWeight + 1);
// get the first group where our random weight is less then element weight
IGrouping<int, T> items = _itemMap
.FirstOrDefault(b => randomWeight <= b.Key);
// since we based our randomWeight on the maxWeight, we better have found one
if (items == null)
{
throw new InvalidOperationException("Internal bug: should have found at least the maxWeight element");
}
// there may be more than one element with this weight; take a random one
T item = items.ElementAt(_random.Next(items.Count()));
return item;
}
}
}

No comments:

Can't RDP? How to enable / disable virtual machine firewall for Azure VM

Oh no!  I accidentally blocked the RDP port on an Azure virtual machine which resulted in not being able to log into the VM anymore.  I did ...