Source code for inferi.probability

"""Contains probability concept classes."""

import random
from fractions import Fraction

[docs]class EventSpace: """An abstract class for objects which are a container of simple events. Any class inheriting from this class should ensure its instances have a ``_simple_events`` property which is a set of :py:class:`.Simple Event` objects.""" def __contains__(self, member): if isinstance(member, Event): return member.simple_events.issubset(self._simple_events) for event in self._simple_events: if event.outcome == member: return True @property def simple_events(self): """The set of simple events in this space. :rtype: ``set``""" return set(self._simple_events)
[docs] def outcomes(self, p=False): """The set of outcomes that the event space's simple events can produce. :param bool p: if ``True``, the results will be returned as a dict with\ probabilities associated. :rtype: ``set`` or ``dict``""" if p: return {e.outcome: e.probability() for e in self._simple_events}
return set([e.outcome for e in self._simple_events])
[docs]class Event(EventSpace): """Base class: :py:class:`.EventSpace` An occurance that is made up of multiple simple events. Events are containers both of their simple events, and the outcomes of those simple events. :param \*events: The :py:class:`.Event` objects within this set. :param str name: The name of the event (default is 'E'). :raises TypeError: if non-events are given. :raises TypeError: if the name is not a string.""" def __init__(self, *events, name="E"): if any(not isinstance(e, Event) for e in events): raise TypeError(f"{events} contains non-events") if not isinstance(name, str): raise TypeError(f"Name {name} is not str") self._simple_events = set() for event in events: self._simple_events.update(event._simple_events) self._name = name def __repr__(self): return f"<{self.__class__.__name__}: {self._name}>" def __or__(self, event): if not isinstance(event, Event): raise TypeError(f"{event} is not an Event") return Event(*(self._simple_events | event._simple_events)) def __and__(self, event): if not isinstance(event, Event): raise TypeError(f"{event} is not an Event") return Event(*(self._simple_events & event._simple_events)) @property def sample_space(self): """The sample space that the event is part of. :rtype: ``SampleSpace``""" for event in self._simple_events: return event._sample_space @property def name(self): """Returns the name of the Event. :rtype: ``str``""" return self._name @property def complement(self): """Returns the complement of the event - the event that this event does not happen. :rtype: ``Event``""" return Event( *(self.sample_space.simple_events - self._simple_events) )
[docs] def probability(self, given=None, fraction=False): """Returns the probability of the event happening. :param Event given: an optional pre-condition to consider. :param bool fraction: If ``True``, the result will be returned as a\ ``Fraction``. :raises TypeError: if the given event is not an :py:class:`.Event` :rtype: ``float``""" if given: if not isinstance(given, Event): raise TypeError(f"{given} is not an event") p = (self & given).probability(fraction=True) p /= given.probability(fraction=True) else: p = sum(event._probability for event in self._simple_events)
return p if fraction else p.numerator / p.denominator
[docs] def mutually_exclusive_with(self, event): """Looks at some other event and checks if this event is mutually exclusive with the other event. That is, whether it is impossible for them both to happen in a given statistical experiment. :param Event event: the other event to check with. :raises TypeError: if a non-Event is given. :rtype: ``bool``""" if not isinstance(event, Event): raise TypeError(f"{event} is not an event")
return not self._simple_events & event._simple_events
[docs] def independent_of(self, event): """Checks to see if this event is independent of some other event - that is, whether its probability is unaffacted by the occurence of the other event. :param Event event: the other event to check with. :raises TypeError: if a non-Event is given. :rtype: ``bool``""" if not isinstance(event, Event): raise TypeError(f"{event} is not an event") return self.probability(fraction=True) == self.probability( fraction=True, given=event
)
[docs] def dependent_on(self, event): """Checks to see if this event is dependent of some other event - that is, whether its probability is affacted by the occurence of the other event. :param Event event: the other event to check with. :raises TypeError: if a non-Event is given. :rtype: ``bool``"""
return not self.independent_of(event)
[docs]class SimpleEvent(Event): """Base class: py:class:`.Event` A simple event - a single outcome of a statistical experiment. :param outcome: The result of this event occuring. :param float probability: The probability of this event occuring. :raises TypeError: if probability isn't numeric. :raises ValueError: if probability is not between 0 and 1.""" def __init__(self, outcome, probability, space): self._outcome = self._name = outcome if not isinstance(probability, (int, float, Fraction)): raise TypeError("probability {} is not numeric".format(probability)) if not 0 <= probability <= 1: raise ValueError("probability {} is invalid".format(probability)) self._probability = probability self._sample_space = space self._simple_events = set((self,)) @property def outcome(self): """The result of this event occuring."""
return self._outcome
[docs]class SampleSpace(EventSpace): """Base class: :py:class:`.EventSpace` The set of all possible things that can result from a statistical experiment. :param \*outcomes: All the possible outcomes. :param dict p: The probabilities for the supplied outcomes. If not given,\ these will be weighted equally. :raises ValueError: if you supply probabilities that don't add up to 1.""" def __init__(self, *outcomes, p=None): if not outcomes and not p: raise ValueError("Sample spaces need at least one outcome") if p is None: p_per_event = Fraction(1, len(outcomes)) fraction_p = {event: p_per_event for event in outcomes} else: fraction_p = {k: Fraction(str(p[k])) for k in p} unaccounted_outcomes = [o for o in outcomes if o not in p] if unaccounted_outcomes: remaining_p = 1 - sum(fraction_p.values()) p_per_event = (remaining_p / len(unaccounted_outcomes)) for e in unaccounted_outcomes: fraction_p[e] = p_per_event if sum(fraction_p.values()) != 1: raise ValueError(f"Probabilities do not add up to 1: {fraction_p}") self._simple_events = set([ SimpleEvent(e, fraction_p[e], self) for e in fraction_p ]) def __repr__(self): return f"<SampleSpace ({len(self._simple_events)} simple events)>"
[docs] def event(self, *outcomes, name=None): """If a single outcome is given, this function will return the :py:class:`.SimpleEvent` corresponding to that outcome. If multiple outcomes are given, the function will return the :py:class:`.Event` corresponding to one of those outcomes occuring. If a callable is provided, the function will return the :py:class:`.Event` of all the simple events that return ``True`` when the callable is applied to their outcome. :param \*outcomes: The outcome(s) to look for. :param str name: The name to apply if a :py:class:`.Event` is returned. :rtype: ``SimpleEvent`` or ``Event``""" if len(outcomes) == 1: if callable(outcomes[0]): f = outcomes[0] outcomes = [ e.outcome for e in self._simple_events if f(e.outcome) ] else: for event in self._simple_events: if event.outcome == outcomes[0]: return event else: return None simple = [e for e in self._simple_events if e.outcome in outcomes]
return Event(*simple, name=name) if name else Event(*simple)
[docs] def chances_of(self, *outcomes): """Returns the probability of the given outcome or outcomes occuring in a single statistical experiment. If multiple outcomes are given, the function will return the :py:class:`.Event` corresponding to one of those outcomes occuring. If a callable is provided, the function will return the :py:class:`.Event` of all the simple events that return ``True`` when the callable is applied to their outcome. :param \*outcomes: The outcome(s) to look for. :rtype: ``float``""" event = self.event(*outcomes)
return event.probability() if event is not None else 0
[docs] def experiment(self): """Generate an outcome.""" outcomes = self.outcomes(p=True) outcomes = [(o, p) for o, p in outcomes.items()] outcomes, odds = zip(*outcomes)
return random.choices(outcomes, weights=odds, k=1)[0]