The error is here:
tp != "none"
tp
is always a list with one element, because random.choices()
returns a list with a single element by default. From the documentation for random.choices()
:
random.choices(population, weights=None, *, cum_weights=None, k=1)
Return a k
sized list of elements chosen from the population with replacement.
With k
left at 1, tp
is going to be a 1-element list each time, and can never be equal to "none"
. It will be equal to ["none"]
, or ["conclusion"]
, and so forth, instead. That means that `tp != "none" is always true.
Next, your any()
test only kicks in if there is more than one nested list with the currently selected value, so at least 2. At that point, you start skipping anything that appeared twice, because the tp != "none"
is always true:
>>> plot_points = [["conclusion", "conclusion"]]
>>> tp = ["conclusion"]
>>> any(plot_points.count(tp) > 1 for tp in plot_points)
True
>>> tp != "none"
True
Your weightings for the choices given make it very, very unlikely that anything other than "conclusion"
is picked. For your 7 options, ["conclusion"]
will be picked 500 out of 506 times you call your turning_point()
function, so the above scenario will occur most of the time (976562500000 out of every 1036579476493 experiments will turn up ["conclusion"]
5 times in a row, or about 33 out of every 35 tests). So you'll extremely rarely will see any of the other options be produced twice, let alone 3 times (only 3 out of every 64777108 tests will repeat any of the other options three times or more).
If you must produce a list in which nothing repeats except for none
, then there is no point in weighting choices. The moment "conclusion"
has been picked, you can't pick it again anyway. If the goal is to make it highly likely that a "conclusion"
element is part of the result, then just make that a separate swap at the end, and just shuffle a list of the remaining choices first. Shuffling lets you cut the result down to size and the first N
elements will all be random, and unique:
>>> import random
>>> action_table = ["confrontation", "protector", "crescendo", "destroy the thing", "meta"]
>>> random.shuffle(action_table) # changes the list order in-place
>>> action_table[:3]
['meta', 'crescendo', 'destroy the thing']
You could pad out that list with "none"
elements to make it long enough to meet the length requirements, and then insert a conclusion
in a random position based on the chances that one should be included:
def plot_points(number):
action_table = ["none", "confrontation", "protector", "crescendo", "destroy the thing", "meta"]
if number > 6:
# add enough `"none"` elements
action_table += ["none"] * (number - 6)
random.shuffle(action_table)
action_table = action_table[:number]
if random.random() > 0.8:
# add in a random "conclusion"
action_table[random.randrange(len(number))] = "conclusion"
return action_table
Note that this is a pseudo-weighted selection; conclusion
is selected 80% of the time, and uniqueness is preserved with only "none"
repeated to pad out the results. You can’t have uniqueness for the other elements otherwise.
However, if you must have
- unique values in the output list (and possibly repeat
"none"
)
- weighted selection of inputs
Then you want a weighted random sample selection without replacement. You can implement this using standard Python libraries:
import heapq
import math
import random
def weighted_random_sample(population, weights, k):
"""Chooses k unique random elements from a population sequence.
The probability of items being selected is based on their weight.
Implementation of the algorithm by Pavlos Efraimidis and Paul
Spirakis, "Weighted random sampling with a reservoir" in
Information Processing Letters 2006. Each element i is selected
by assigning ids with the formula R^(1/w_i), with w_i the weight
for that item, and the top k ids are selected.
"""
if not 0 <= k < len(population):
raise ValueError("Sample larger than population or negative")
if len(weights) != len(population):
raise ValueError("The number of weights does not match the population")
key = lambda iw: math.pow(random.random(), 1 / iw[1])
decorated = heapq.nlargest(k, zip(population, weights), key=key)
return [item for item, _ in decorated]
Use this to select your items if you need 7 items or fewer, otherwise and extra "none"
values and just shuffle (as all 7 items end up selected anyway):
def plot_points(number):
action_table = ["conclusion", "none", "confrontation", "protector", "crescendo", "destroy the thing", "meta"]
if number > len(action_table):
# more items than are available
# pad out with `"none"` elements and shuffle
action_table += ["none"] * (number - len(action_table))
random.shuffle(action_table)
return action_table
weights = [3, 1, 1, 1, 2, 2, 1]
return weighted_random_sample(action_table, weights, number)
Demo:
>>> plot_points(5)
['none', 'conclusion', 'meta', 'crescendo', 'destroy the thing']
>>> plot_points(5)
['conclusion', 'destroy the thing', 'crescendo', 'meta', 'confrontation']
>>> plot_points(10)
['none', 'crescendo', 'protector', 'confrontation', 'meta', 'destroy the thing', 'none', 'conclusion', 'none', 'none']
Of course, if your real action_table
is much larger and you disallow picking more plot points than you have actions, there is no need to pad things out at all and you'd just use weighted_random_sample()
directly.