Powered by AppSignal & Oban Pro

Weighted Random Dev

guides/random_dev.livemd

Weighted Random Dev

Mix.install(
  [
    {:weighted_random, path: Path.join(__DIR__, "../"), env: :dev},
    {:tucan, "~> 0.6.0"},
    {:vega_lite, "~> 0.1.0"},
    {:kino_vega_lite, "~> 0.1.0"},
  ],
  config_path: :weighted_random,
  lockfile: :weighted_random
)

Weighted Random tutorial

# Before getting into the nitty gritty, let's start with a visual demo.
# Without any weights, every outcome has an equal chance of being drawn.
# For example, when picking a random number between 1-10, each outcome has a 10% chance.

# Notice how the lines are pretty even


#### Controls ####
outcomes = 0..4
sample_size = 1000
####


weights = []
table = WeightedRandom.preprocess(outcomes, weights)
results = WeightedRandom.take(table, sample_size)

# This is equal to:
# results = for 1..sample_size do
#   Enum.random(outcomes)
# end

results
|> WeightedRandom.Utils.Plotting.results_to_bars()
|> Tucan.bar("outcome", "hits", height: 200, width: 500, fill_color: "#33245A", corner_radius: 5)
# Watch what happens when the number 3 is 10x more likely to appear than any other number

#### Controls ####
outcomes = 0..4
sample_size = 1000
weights = [
  %{target: 3, weight: 10}
]
####


table = WeightedRandom.preprocess(outcomes, weights)
results = WeightedRandom.take(table, sample_size)

results
|> WeightedRandom.Utils.Plotting.results_to_bars()
|> Tucan.bar("outcome", "hits", height: 200, width: 500, fill_color: "#33245A", corner_radius: 5)
# Another way to do that is to use a list of probabilities, 
# instead of outcomes + weights


#### Controls ####
sample_size = 1000
probabilities = [
  0.01, 0.01, 0.97, 0.01
]

# alternately, as fractions
# probabilities = [1 / 100, 1 / 100, 97 / 100, 1 / 100]

####

# Notice we use `preprocess_p/1` instead of `preprocess/1`
# The `_p` is for probabilities.
table = WeightedRandom.preprocess_p(probabilities)
results = WeightedRandom.take(table, sample_size)

results
|> WeightedRandom.Utils.Plotting.results_to_bars()
|> Tucan.bar("outcome", "hits", height: 200, width: 500, fill_color: "#33245A", corner_radius: 5)
# We can have more than one weight, too


#### Controls ####
sample_size = 1000
outcomes = 0..20
weights = [
  %{target: 6, weight: 5},
  %{target: 15, weight: 5}
]
####

table = WeightedRandom.preprocess(outcomes, weights)
results = WeightedRandom.take(table, sample_size)

results
|> WeightedRandom.Utils.Plotting.results_to_bars()
|> Tucan.bar("outcome", "hits", height: 200, width: 500, fill_color: "#33245A", corner_radius: 5)
#### Using the Curves library  ####

# WeightedRandom weights can easily follow bezier curves.
# let's set up a select list to be used in the next code block    
# That way, we can pick different curves and watch how they affect the randomness.
types = Curves.Bezier.Predefined.list()
|> Enum.map(&{&1, &1})

bezier_type = Kino.Input.select("Curve Type", types, default: :ease_in_out)
# By using different predefined curves, we clearly get very distinct shapes
# (Of course, some curves work better than others when doing this)

#### Controls ####
length = 100
outcomes = 0..length
sample_size = 1_000_000
curve = Kino.Input.read(bezier_type) # Use the select list above this code block
####

weights = [%{target: round(length / 2), weight: 100, left_dist: round(length / 4), right_dist: round(length / 4), curve: curve}]
table = WeightedRandom.preprocess(outcomes, weights)
results = WeightedRandom.take(table, sample_size)

results
|> WeightedRandom.Utils.Plotting.results_to_bars()
|> Tucan.bar("outcome", "hits", height: 300, width: 300, fill_color: "#33245A", corner_radius: 5)
# So you can also use your own custom bezier curve.

# Unfortunately, due to some 'mathy' reasons about probabilities not being
#   the same as coordinates on a graph, your results will often look 'flatter' or 
#   more linear than the actual bezier curve.

# So it is best to keep your curves simple and not rely too much on matching them


#### Controls ####
length = 100
outcomes = 0..length
sample_size = 1_000_000
curve = [
  {0, 0},
  {0.33, -4},
  {0.67, 4},
  {1, 1}
]

####
weights = [%{target: round(length / 2), weight: 100, left_dist: round(length / 4), right_dist: round(length / 4), curve: curve}]
table = WeightedRandom.preprocess(outcomes, weights)
results = WeightedRandom.take(table, sample_size)

results
|> WeightedRandom.Utils.Plotting.results_to_bars()
|> Tucan.bar("outcome", "hits", height: 300, width: 300, fill_color: "#33245A", corner_radius: 5)
## Dice
alias WeightedRandom.Dice
import Dice

# This creates 4 x 6-sided dice
# In standard dice notation this would be written as "4d6"
d = ~d{4, 6}

# Now let's make the number 2 have more weight
# Dice always use outcome_type: :value, not :index, so the target is 2
weights = [%{target: 2, amount: 50}]
d = Dice.add_weight(d, weights)
d = Dice.roll(d)

IO.inspect(Dice.results(d), label: "results")
d.total
# We can also add modifiers to the dice notation.
# for example 2d8+1 would create 2 x 8-sided dice, and add +1 to the total
d = ~d{2, 8, 1}
    |> Dice.roll()

IO.inspect(Dice.results(d), label: "results")
d.total