WeightedRandom.Backend behaviour (weighted_random v1.0.0)

Copy Markdown View Source

WeightedRandom.Backend offers a contract to all who implement it:

  1. The main WeightedRandom package presents a novel interface for generating a list of probabilities or weights
  2. 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.Module

Developing 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 

end

This 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])
=> 3

Testing 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

backend_opts()

@type backend_opts() :: [{:probability_type, probability_type()}]

index()

@type index() :: integer()

indices()

@type indices() :: [index()]

opts()

@type opts() :: keyword()

percentage()

@type percentage() :: float()

probabilities()

@type probabilities() :: [percentage()]

probability_type()

@type probability_type() :: :weights | :probabilities

resolved_weights()

@type resolved_weights() :: [float()]

t()

@type t() :: %WeightedRandom.Backend{
  backend: atom(),
  outcomes: list(),
  table: struct()
}

table()

@type table() :: struct()

Callbacks

options()

(optional)
@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.

preprocess(input, opts)

@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.

take(table, count)

@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.