WeightedRandom.Backend offers a contract to all who implement it:
- The main WeightedRandom package presents a novel interface for generating a list of probabilities or weights
- The custom Backend module decides what to do with those probabilities once they are generated.
Using Backends
To use a different backend, you can either pass it in as an opt (see function docs), or you can put it in your config file:
config :weighted_random,
backend: Your.Backend.ModuleDeveloping a new backend
To create your own backend, copy and change an existing one (like WeightedRandom.Backend.WalkerAlias or WeightedRandom.Backend.Linear).
Your module must implement the callbacks listed below: preprocess/2, take/2, and optionally options/0.
The simplest backend might look like this.
defmodule My.Backend do
use WeightedRandom.Backend
defstruct [:list]
@impl true
def preprocess(probabilities, _opts) do
struct(__MODULE__, %{list: probabilities})
end
@impl true
def take(%__MODULE__{list: li}, count) do
for _ <- 1..count do
Enum.random(li)
end
end
endThis backend doesn't actually apply probability, it is just a glorified Enum.rand(list) for now.
But you can use it in any of the main functions that take an opt of backend
WeightedRandom.rand(0..10, weights, [backend: My.Backend])
=> 3Testing a backend
Testing random numbers is really hard. Even when you use seeds, it can be very brittle.
The WeightedRandom.Utils.Analysis module will be very useful for ensuring that your results follow the expected probabilities.
defmodule My.BackendTest do
use ExUnit.Case
alias WeightedRandom.Utils.Analysis
alias My.Backend, as: Mod
test "should return accurate sample" do
probabilities = [0.25, 0.25, 0.5]
sample = WeightedRandom.rand_p(probabilities, backend: Mod, take: 1000)
tolerance = 0.05
assert Analysis.match_probability?(probabilities, sample, tolerance)
end
end
Summary
Callbacks
A keyword list of options that the backend requires from the WeightedRandom library.
input is a list of floats.
Given the struct returned by preprocess/2, return a list of random indices equal to count.
Types
@type backend_opts() :: [{:probability_type, probability_type()}]
@type index() :: integer()
@type indices() :: [index()]
@type opts() :: keyword()
@type percentage() :: float()
@type probabilities() :: [percentage()]
@type probability_type() :: :weights | :probabilities
@type resolved_weights() :: [float()]
@type table() :: struct()
Callbacks
@callback options() :: backend_opts()
A keyword list of options that the backend requires from the WeightedRandom library.
Currently the only option is :probability_type, which influences the input argument of preprocess/2.
When set to :probabilities (default), the input will be a list of floats that roughly sum to 1.0.
When set to :weights, the input will be a list of floats.
@callback preprocess(input :: probabilities() | resolved_weights(), opts :: opts()) :: table()
input is a list of floats.
This function must return some kind of struct that will later be passed into take/2.
For details about opts, see WeightedRandom.preprocess and WeightedRandom.preprocess_p.
@callback take(table :: struct(), count :: pos_integer()) :: indices :: indices()
Given the struct returned by preprocess/2, return a list of random indices equal to count.