やりたかったこと
pythonのrandom.choicesのような重み付きのランダム要素選択をC#でもしたい。
車輪の再発明感がすごいが勉強になったのでヨシ!
仕様
重み(weights)の累積和を要素のコレクション(items)と紐づけ、乱数が累積和の値を超えた最初の要素を返却する仕様とした。
ポイント
itemsとweightsの個数が合わない(割り当てられている重みが存在しない場合)は重みが0とみなせば当選することはないので、Zipメソッドが個数が少ない方に合わせて出力されることを利用して簡潔に書くことができる。
コード
WeightedRoulette.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace YourNamespace
{
public class WeightedRoulette
{
private readonly Random _random;
public WeightedRoulette(Random random = null)
{
_random = random ?? new Random();
}
public T Roll<T>(IEnumerable<T> items, IEnumerable<double> weights)
{
if (items is null) return default(T);
if(weights is null) return items.First();
double sum = 0d;
// 累積和の計算が遅延実行されないようにToArrayする
IEnumerable<double> cumlate = weights.Select(v => sum += Math.Max(0d, v)).ToArray();
var roulette = items.Zip(cumlate, (s, c) => (s, c));
double value = _random.NextDouble() * sum;
return roulette.First(r => r.c >= value).s;
}
public int Roll(IEnumerable<double> weights) => Roll(Enumerable.Range(0, weights?.Count() ?? 1), weights);
}
}