From 5e9348173861c4502d4fbb1991a81e971f1a8082 Mon Sep 17 00:00:00 2001 From: Peter Wildeford Date: Sat, 12 Aug 2023 16:53:59 -0400 Subject: [PATCH 1/3] Started dice_pool --- CHANGES.md | 1 + README.md | 2 -- squigglepy/__init__.py | 1 + squigglepy/dice.py | 82 ++++++++++++++++++++++++++++++++++++++++++ squigglepy/samplers.py | 30 ++++++++++++++++ squigglepy/utils.py | 66 ---------------------------------- 6 files changed, 114 insertions(+), 68 deletions(-) create mode 100644 squigglepy/dice.py diff --git a/CHANGES.md b/CHANGES.md index 9697481..257437b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ * **[Breaking change]** This package now only supports Python 3.9 and higher. * **[Breaking change]** `get_percentiles` and `get_log_percentiles` now always return a dictionary, even if there's only one element. * **[Breaking change]** `.type` is now removed from distribution objects. +* **[Breaking change]** `sq.flip_coin` and `sq.roll_die` as functions are now functionally replaced by sampling from `sq.coin` or `sq.die` distribution objects. * Package load time is now ~2x faster. * Pandas and matplotlib as removed as required dependencies, but their related features are lazily enabled when the modules are available. These packages are still available for install as extras, installable with `pip install squigglepy[plots]` (for plotting-related functionality, matplotlib for now), `pip install squigglepy[ecosystem]` (for pandas, and in the future other related packages), or `pip install squigglepy[all]` (for all extras). * Multicore distribution now does extra checks to avoid crashing from race conditions. diff --git a/README.md b/README.md index e3d5011..3979808 100644 --- a/README.md +++ b/README.md @@ -181,8 +181,6 @@ roll_die(sides=6, n=10) # [2, 6, 5, 2, 6, 2, 3, 1, 5, 2] ``` -This is already included standard in the utils of this package. Use `sq.roll_die`. - ### Bayesian inference 1% of women at age forty who participate in routine screening have breast cancer. diff --git a/squigglepy/__init__.py b/squigglepy/__init__.py index f885e8b..189acd0 100644 --- a/squigglepy/__init__.py +++ b/squigglepy/__init__.py @@ -1,4 +1,5 @@ from .distributions import * # noqa ignore=F405 +from .dice import * # noqa ignore=F405 from .numbers import * # noqa ignore=F405 from .samplers import * # noqa ignore=F405 from .utils import * # noqa ignore=F405 diff --git a/squigglepy/dice.py b/squigglepy/dice.py new file mode 100644 index 0000000..26ac1b1 --- /dev/null +++ b/squigglepy/dice.py @@ -0,0 +1,82 @@ +from .utils import is_dist +from .distributions import OperableDistribution + + +class Die(OperableDistribution): + def __init__(self, sides=None, explode_on=None): + super().__init__() + + if is_dist(sides) or callable(sides): + from .samplers import sample + + sides = sample(sides) + elif sides < 2: + raise ValueError("cannot roll less than a 2-sided die.") + elif not isinstance(sides, int): + raise ValueError("can only roll an integer number of sides") + elif explode_on is not None: + if not isinstance(explode_on, list): + explode_on = [explode_on] + if len(explode_on) >= sides: + raise ValueError("cannot explode on every value") + self.sides = sides + self.explode_on = explode_on + + def __str__(self): + explode_out = ( + "" if self.explode_on is None else ", explodes on {}".format(str(self.explode_on)) + ) + out = " Die({}{})".format(self.sides, explode_out) + return out + + +def die(sides, explode_on=None): + """ + Create a distribution for a die. + + Parameters + ---------- + sides : int + The number of sides of the die that is rolled. + + explode_on : list or int + An additional die will be rolled if the initial die rolls any of these values. + Implements "exploding dice". + + Returns + ------- + Distribution + A distribution that models a coin flip, returning either "heads" or "tails" + + Examples + -------- + >>> die(6) + Die(6) + """ + return Die(sides=sides, explode_on=explode_on) + + +class Coin(OperableDistribution): + def __init__(self): + super().__init__() + + def __str__(self): + out = " Coin" + return out + + +def coin(): + """ + Create a distribution for a coin + + Returns + ------- + Distribution + A distribution that models a coin flip, returning either "heads" or "tails" + + Examples + -------- + >>> coin() + Coin + """ + return Coin() diff --git a/squigglepy/samplers.py b/squigglepy/samplers.py index 4bb40a2..0a53123 100644 --- a/squigglepy/samplers.py +++ b/squigglepy/samplers.py @@ -42,6 +42,8 @@ const, ) +from .dice import Coin, Die + _squigglepy_internal_sample_caches = {} @@ -914,6 +916,34 @@ def run_dist(dist, pbar=None, tick=1): _multicore_tqdm_cores=_multicore_tqdm_cores, ) + elif isinstance(dist, Die): + samples = discrete_sample( + list(range(1, dist.sides + 1)), + samples=n, + _multicore_tqdm_n=_multicore_tqdm_n, + _multicore_tqdm_cores=_multicore_tqdm_cores, + ) + if dist.explode_on is not None: + samples = _enlist(samples) + explosion_samples = samples + n_explosions = 1 + while n_explosions > 0: + explosion_samples = _enlist(explosion_samples) + n_explosions = sum([s in dist.explode_on for s in explosion_samples]) + if n_explosions > 0: + explosion_samples = discrete_sample( + list(range(1, dist.sides + 1)), + samples=n_explosions) + samples.append(explosion_samples) + + elif isinstance(dist, Coin): + samples = discrete_sample( + ["heads", "tails"], + samples=n, + _multicore_tqdm_n=_multicore_tqdm_n, + _multicore_tqdm_cores=_multicore_tqdm_cores, + ) + elif isinstance(dist, NormalDistribution): samples = normal_sample(mean=dist.mean, sd=dist.sd, samples=n) diff --git a/squigglepy/utils.py b/squigglepy/utils.py index f07e1da..bbdc31c 100644 --- a/squigglepy/utils.py +++ b/squigglepy/utils.py @@ -799,72 +799,6 @@ def doubling_time_to_growth_rate(doubling_time): return math.exp(math.log(2) / doubling_time) - 1 -def roll_die(sides, n=1): - """ - Roll a die. - - Parameters - ---------- - sides : int - The number of sides of the die that is rolled. - n : int - The number of dice to be rolled. - - Returns - ------- - int or list - Returns the value of each die roll. - - Examples - -------- - >>> set_seed(42) - >>> roll_die(6) - 5 - """ - if is_dist(sides) or callable(sides): - from .samplers import sample - - sides = sample(sides) - if not isinstance(n, int): - raise ValueError("can only roll an integer number of times") - elif sides < 2: - raise ValueError("cannot roll less than a 2-sided die.") - elif not isinstance(sides, int): - raise ValueError("can only roll an integer number of sides") - else: - from .samplers import sample - from .distributions import discrete - - return sample(discrete(list(range(1, sides + 1))), n=n) if sides > 0 else None - - -def flip_coin(n=1): - """ - Flip a coin. - - Parameters - ---------- - n : int - The number of coins to be flipped. - - Returns - ------- - str or list - Returns the value of each coin flip, as either "heads" or "tails" - - Examples - -------- - >>> set_seed(42) - >>> flip_coin() - 'heads' - """ - rolls = roll_die(2, n=n) - if isinstance(rolls, int): - rolls = [rolls] - flips = ["heads" if d == 2 else "tails" for d in rolls] - return flips[0] if len(flips) == 1 else flips - - def kelly(my_price, market_price, deference=0, bankroll=1, resolve_date=None, current=0): """ Calculate the Kelly criterion. From ad94f32944df7eff2cc4f18946f795167300281e Mon Sep 17 00:00:00 2001 From: Peter Wildeford Date: Sat, 12 Aug 2023 16:56:05 -0400 Subject: [PATCH 2/3] reformat --- squigglepy/samplers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squigglepy/samplers.py b/squigglepy/samplers.py index 0a53123..4e7c1ff 100644 --- a/squigglepy/samplers.py +++ b/squigglepy/samplers.py @@ -932,8 +932,8 @@ def run_dist(dist, pbar=None, tick=1): n_explosions = sum([s in dist.explode_on for s in explosion_samples]) if n_explosions > 0: explosion_samples = discrete_sample( - list(range(1, dist.sides + 1)), - samples=n_explosions) + list(range(1, dist.sides + 1)), samples=n_explosions + ) samples.append(explosion_samples) elif isinstance(dist, Coin): From d7bbea79b5f756442fdb0309cbe4240d2b98ece2 Mon Sep 17 00:00:00 2001 From: Peter Wildeford Date: Sat, 12 Aug 2023 18:57:07 -0400 Subject: [PATCH 3/3] explain built-in --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3979808..6b850a0 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,9 @@ roll_die(sides=6, n=10) # [2, 6, 5, 2, 6, 2, 3, 1, 5, 2] ``` +(This is a toy example. For better dice rolling, use the built-in `sq.die`) + + ### Bayesian inference 1% of women at age forty who participate in routine screening have breast cancer. @@ -394,10 +397,10 @@ from squigglepy.numbers import K, M, B, T from squigglepy import bayes def define_event(): - if sq.flip_coin() == 'heads': # Blue bag - return sq.roll_die(6) + if ~sq.coin() == 'heads': # Blue bag + return sq.die(6) else: # Red bag - return sq.discrete([4, 6, 10, 20]) >> sq.roll_die + return sq.discrete([4, 6, 10, 20]) >> sq.die bayes.bayesnet(define_event,