Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating random integers with given probabilities

Tags:

random

haskell

I need to generate an infinite stream of random integers, with numbers to be in range [1..n]. However the probability for each number p_i is given in advance thus the distribution is not uniform.

Is there a library function to do it in Haskell?

like image 985
Yrogirg Avatar asked Jan 09 '12 08:01

Yrogirg


2 Answers

As people have pointed out there is a function in Control.Monad.Random, but it has pretty poor complexity. Here is some code that I by some coincidence wrote this morning. It uses the beautiful Alias algorithm.

module Data.Random.Distribution.NonUniform(randomsDist) where
import Data.Array
import Data.List
import System.Random

genTable :: (Num a, Ord a) => [a] -> (Array Int a, Array Int Int)
genTable ps =
    let n = length ps
        n' = fromIntegral n
        (small, large) = partition ((< 1) . snd) $ zip [0..] $ map (n' *) ps
        loop ((l, pl):ls) ((g, pg):gs) probs aliases =
            let prob = (l,pl)
                alias = (l,g)
                pg' = (pg + pl) - 1
                gpg = (g, pg')
            in  if pg' < 1 then loop (gpg:ls) gs (prob:probs) (alias:aliases)
                          else loop ls (gpg:gs) (prob:probs) (alias:aliases)
        loop ls gs probs aliases = loop' (ls ++ gs) probs aliases
        loop' [] probs aliases = (array (0,n-1) probs, array (0,n-1) aliases)
        loop' ((g,_):gs) probs aliases = loop' gs ((g,1):probs) ((g, -1):aliases)
    in  loop small large [] []

-- | Generate an infinite list of random values with the given distribution.
-- The probabilities are scaled so they do not have to add up to one.
-- 
-- Uses Vose's alias method for generating the values.
-- For /n/ values this has O(/n/) setup complexity and O(1) complexity for each
-- generated item.
randomsDist :: (RandomGen g, Random r, Fractional r, Ord r)
            => g                           -- | random number generator
            -> [(a, r)]                    -- | list of values with the probabilities
            -> [a]
randomsDist g xps =
    let (xs, ps) = unzip xps
        n = length xps
        axs = listArray (0, n-1) xs
        s = sum ps
        (probs, aliases) = genTable $ map (/ s) ps
        (g', g'') = split g
        is = randomRs (0, n-1) g'
        rs = randoms g''
        ks = zipWith (\ i r -> if r <= probs!i then i else aliases!i) is rs
    in  map (axs!) ks
like image 194
augustss Avatar answered Sep 27 '22 18:09

augustss


Just to expand on dflemstr's answer, you can create an infinite list of weighted values using Control.Monad.Random like this:

import Control.Monad.Random
import System.Random

weightedList :: RandomGen g => g -> [(a, Rational)] -> [a]
weightedList gen weights = evalRand m gen
    where m = sequence . repeat . fromList $ weights

And use it like this:

> let values = weightedList (mkStdGen 123) [(1, 2), (2, 5), (3, 10)]
> take 20 values
[2,1,3,2,1,2,2,3,3,3,3,3,3,2,3,3,2,2,2,3]

This doesn't require the IO monad, but you need to provide the random number generator that's used for the stream.

like image 32
shang Avatar answered Sep 27 '22 16:09

shang