How to return a shuffled list considering mutually exclusive items?

Viewed 234

Say I have a list of options and I want to pick a certain number randomly.

In my case, say the options are in a list ['a', 'b', 'c', 'd', 'e'] and I want my script to return 3 elements.

However, there is also the case of two options that cannot appear at the same time. That is, if option 'a' is picked randomly, then option 'b' cannot be picked. And the same applies the other way round.

So valid outputs are: ['a', 'c', 'd'] or ['c', 'd', 'b'], while things like ['a', 'b', 'c'] would not because they contain both 'a' and 'b'.

To fulfil these requirements, I am fetching 3 options plus another one to compensate a possible discard. Then, I keep a set() with the mutually exclusive condition and keep removing from it and check if both elements have been picked or not:

import random


mutually_exclusive = set({'a', 'b'})
options = ['a', 'b', 'c', 'd', 'e']
num_options_to_return = 3

shuffled_options = random.sample(options, num_options_to_return + 1)

elements_returned = 0
for item in shuffled_options:
    if elements_returned >= num_options_to_return:
        break

    if item in mutually_exclusive:
        mutually_exclusive.remove(item)
        if not mutually_exclusive:
            # if both elements have appeared, then the set is empty so we cannot return the current value
            continue

    print(item)
    elements_returned += 1

However, I may be overcoding and Python may have better ways to handle these requirements. Going through random's documentation I couldn't find ways to do this out of the box. Is there a better solution than my current one?

6 Answers

One way to do this is use itertools.combinations to create all of the possible results, filter out the invalid ones and make a random.choice from that:

>>> from itertools import combinations
>>> from random import choice
>>> def is_valid(t):
...     return 'a' not in t or 'b' not in t
... 
>>> choice([
...     t 
...     for t in combinations('abcde', 3) 
...     if is_valid(t)
... ])
... 
('c', 'd', 'e')

Maybe a bit naive, but you could generate samples until your condition is met:

import random

options = ['a', 'b', 'c', 'd', 'e']
num_options_to_return = 3
mutually_exclusive = set({'a', 'b'})

while True:
    shuffled_options = random.sample(options, num_options_to_return)
    if all (item not in mutually_exclusive for item in shuffled_options):
        break

print(shuffled_options)

I would implement it with sets:

import random

mutually_exclusive = {'a', 'b'}
options = ['a', 'b', 'c', 'd', 'e']
num_options_to_return = 3

while True:
    s = random.sample(options, num_options_to_return)
    print('Sample is', s)
    if not mutually_exclusive.issubset(s):
        break
    print('Discard!')

print('Final sample:', s)

Prints (for example):

Sample is ['a', 'b', 'd']
Discard!
Sample is ['b', 'a', 'd']
Discard!
Sample is ['e', 'a', 'c']
Final sample: ['e', 'a', 'c']

You can restructure your options.

import random

options = [('a', 'b'), 'c', 'd', 'e']
n_options = 3

selected_option = random.sample(options, n_options)

result = [item if not isinstance(item, tuple) else random.choice(item)
          for item in selected_option]

print(result)

I created the below function and I think it's worth sharing it too ;-)

def random_picker(options, n, mutually_exclusives=None):
    if mutually_exclusives is None:
        return random.sample(options, n)
    elif any(len(pair) != 2 for pair in mutually_exclusives):
        raise ValueError('Lenght of pairs of mutually_exclusives iterable, must be 2')

    res = []
    while len(res) < n:
        item_index = random.randint(0, len(options) - 1)
        item = options[item_index]
        if any(item == exc and pair[-(i - 1)] in res for pair in mutually_exclusives
               for i, exc in enumerate(pair)):
            continue
        res.append(options.pop(item_index))
    return res

Where:

  • options is the list of available options to pick from.
  • n is the number of items you want to be picked from options
  • mutually_exclusives is an iterable containing tuples pairs of mutually exclusive items

You can use it as follows:

>>> random_picker(['a', 'b', 'c', 'd', 'e'], 3)
['c', 'e', 'a']
>>> random_picker(['a', 'b', 'c', 'd', 'e'], 3, [('a', 'b')])
['d', 'b', 'e']
>>> random_picker(['a', 'b', 'c', 'd', 'e'], 3, [('a', 'b'), ('a', 'c')])
['e', 'd', 'a']
import random

l = [['a','b'], ['c'], ['d'], ['e']]
x = [random.choice(i) for i in random.sample(l,3)]

here l is a two-dimensional list, where the fist level reflects an and relation and the second level an or relation.

Related