9
\$\begingroup\$

Task:

Get a random event from a set of collectively exhaustive and mutually exclusive events, with this set being represented by a list of tuples (name, probability).

Reminder:

  • Collectively exhaustive events means at least one of them must occur.
  • Mutually exclusive events means at most 1 of them occurs.
  • Collectively exhaustive and mutually exclusive means their probabilities sum up to 1 and only one of them occurs.

Code:

Events definition:

events = [
    ('Win', 0.5),
    ('Loss', 0.3),
    ('Draw', 0.2)
]

Getting the random event:

def get_random_event(events):
    random_number = uniform(0., 1.)
    lower_bound = 0
    for event, prob in events:
        if lower_bound <= random_number <= lower_bound + prob:
            return event
        else:
            lower_bound += prob

Illustration:

0 -- -- Win -- -- 0.5 -- Loss -- 0.8 - Draw - 1

Generate a random number in [0,1] and check where it falls.

\$\endgroup\$
  • \$\begingroup\$ This only works because of the ordering of the data in events , which isn't very obvious. Do you have a check that ensures that it's ordered in by prob? \$\endgroup\$ – Ben Jan 1 '18 at 22:46
  • 1
    \$\begingroup\$ Order does not matter at all here. @Ben \$\endgroup\$ – Adel Redjimi Jan 1 '18 at 23:43
10
\$\begingroup\$

Some things:

  1. The collectively exhaustive events are not going to change or be updated. So use a tuple instead of a list:

    events = (  # <<- Note: ( not [ 
        ('Win', 50),
        ...
    )
    
  2. You don't show your import of random. But I'd prefer that you not import uniform by name, and rather spell it out inside your function. You're only calling it once, so it's more efficient to the reader for you to simply say:

    random_number = random.uniform(...)
    

    than to have them search backwards for where uniform occurs. Also, of course, in this context random.uniform actually reads better than just uniform.

  3. You have an input parameter events. You use event, prob in your loop. But event is the singular of events, so when you say return event it seems like you are returning a member of the events iterable. I'd choose a different name, like outcome, making it clear that your returning a component of the inner tuple.

  4. You compare lower_bound <= random_number <= lower_bound + prob but you don't have to. Since your random number is pinned to 0., you could just use a comparison against the high value and get the same result.

  5. Structurally, the lower_bound += prob shouldn't be in the else branch. Yes, it only executes when the if is not executed, but the if should not be part of the flow for that statement. I'd rather see:

    if ...
        return ...
    
    lower_bound += prob
    

    This kind of thing comes back to bite you when you're refactoring. Or, worse yet, when someone else is refactoring. (Imagine you changed the return to a print, or something. Suddenly the code stops working.)

  6. The Task description would (almost) make a good function docblock. Note: s/list/iterable/:

    from typing import Iterable, Tuple
    
    Name = str
    Prob = float
    
    def get_random_event(events: Iterable[Tuple[Name, Prob]]) -> Name:
        """Get a random event from a set of collectively exhaustive 
        and mutually exclusive events, with this set being represented
        by an iterable of tuples (name, probability). 
    
        """
    
| improve this answer | |
\$\endgroup\$
4
\$\begingroup\$

If you are using a recent version of Python (3.6+), the behaviour you're seeking is already implemented in random.choices.

Basically, all you need to do is to split the event list of couples into two lists and feed these two lists to the choices function. zip is there for you:

def get_random_event(events):
    event, = random.choices(*zip(*events))
    return event
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ @Graipher I thought it was pretty obvious from the docs, but added it in the answer anyway. \$\endgroup\$ – 301_Moved_Permanently Jan 2 '18 at 14:01
  • \$\begingroup\$ Yes, but I had to click the link first :). Did not know about this new function until now, this is a nice addition to the random module. \$\endgroup\$ – Graipher Jan 2 '18 at 14:03
  • \$\begingroup\$ Can I import this function, somehow, to Python 2.7 ? \$\endgroup\$ – Adel Redjimi Jan 2 '18 at 16:34
  • 1
    \$\begingroup\$ @AdelRedjimi you might be able to use the implementation from the standard library \$\endgroup\$ – 301_Moved_Permanently Jan 2 '18 at 20:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.