ProbabilityPewter 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ """
2
+ Docstring for ProbabilityPewter package
3
+ ========================================
4
+
5
+ This package provides tools for simulating and visualising probability, primarily aimed at RPGs but also useful for other applications.
6
+ It includes functions for rolling dice, generating random statistics, and visualizing probability distributions.
7
+
8
+ """
9
+
10
+ __version__ = "0.5.0"
11
+
12
+
13
+ #Reminder: list functions here that are main features of the package, for easier access.
14
+ # For more niche functions or helper functions, users can do a full import from the submodules.
15
+ from .calculator import combined_prob
16
+ from .roll_stats import rpg_dice, prob_table, combat_odds
17
+ from .visualiser import plot_dice, plot_normal, plot_combat
18
+
19
+ #Finally, we can define __all__ to list the public API of the package.
20
+ # This is optional, but it can help with tools like autocomplete in IDEs, and it also serves as documentation for users about what the main features of the package are.
21
+ __all__ = [
22
+ "__version__",
23
+ "combined_prob",
24
+ "rpg_dice",
25
+ "prob_table",
26
+ "combat_odds",
27
+ "plot_dice",
28
+ "plot_normal",
29
+ "plot_combat",
30
+ ]
@@ -0,0 +1,5 @@
1
+ """Probability calculation tools for ProbabilityPewter."""
2
+
3
+ from .combiner import combined_prob
4
+
5
+ __all__ = ["combined_prob"]
@@ -0,0 +1,46 @@
1
+ """
2
+ Docstring for the bayes module
3
+
4
+ This module provides functionality to update probabilities using Bayes' theorem.
5
+
6
+ """
7
+
8
+
9
+ def bayes_updater(prior_A, likelihood_B_given_A, likelihood_B_given_not_A):
10
+ """Calculate the posterior probability of A given B, P(A|B), using Bayes' theorem.
11
+
12
+ Parameters:
13
+ -----------
14
+ prior_A : float
15
+ Prior probability P(A) - between 0 and 1.
16
+ likelihood_B_given_A : float
17
+ Probability of B given A, P(B|A) - between 0 and 1.
18
+ likelihood_B_given_not_A : float
19
+ Probability of B given not A, P(B|not A) - between 0 and 1.
20
+
21
+ Returns:
22
+ --------
23
+ float
24
+ Posterior probability P(A|B) - between 0 and 1.
25
+ """
26
+ values = {
27
+ "prior_A": prior_A,
28
+ "likelihood_B_given_A": likelihood_B_given_A,
29
+ "likelihood_B_given_not_A": likelihood_B_given_not_A,
30
+ }
31
+
32
+ for name, value in values.items():
33
+ if not isinstance(value, (int, float)):
34
+ raise TypeError(f"{name} must be numeric, got {type(value).__name__}.")
35
+ if value < 0 or value > 1:
36
+ raise ValueError(f"{name} must be between 0 and 1.")
37
+
38
+ prior_not_A = 1 - prior_A
39
+ numerator = likelihood_B_given_A * prior_A
40
+ denominator = numerator + (likelihood_B_given_not_A * prior_not_A)
41
+
42
+ if denominator == 0:
43
+ raise ValueError("Denominator is zero, check input probabilities.")
44
+
45
+ posterior_A_given_B = numerator / denominator
46
+ return posterior_A_given_B
@@ -0,0 +1,143 @@
1
+ """
2
+ Docstring for the combiner module
3
+
4
+ This module provides functionality to calculate combined probabilities and outcomes.
5
+
6
+ """
7
+ import math
8
+ from fractions import Fraction
9
+
10
+
11
+ def _round_sig(val, sig=10):
12
+ """Round val to sig significant figures, preserving very small values."""
13
+ if val == 0:
14
+ return 0.0
15
+ magnitude = math.floor(math.log10(abs(val)))
16
+ return round(val, sig - 1 - magnitude)
17
+
18
+
19
+ def _to_decimal_str(val):
20
+ """Format a float as a plain decimal string without scientific notation."""
21
+ if val == 0:
22
+ return "0"
23
+ magnitude = math.floor(math.log10(abs(val)))
24
+ decimal_places = max(0, 10 - 1 - magnitude)
25
+ formatted = f"{val:.{decimal_places}f}"
26
+ if "." in formatted:
27
+ formatted = formatted.rstrip("0").rstrip(".")
28
+ return formatted
29
+
30
+
31
+ def combined_prob(A=0.5, B=0.33, event="A&B", output_scale="prob"):
32
+ """
33
+ Calculate the combined probability of independent events, like A and B.
34
+ The syntax for the event parameter specifies the relationship between events. For example: "A&B" means the probability that both A and B occur, while "A/B" means that either A or B occurs.
35
+
36
+
37
+ Parameters:
38
+ -----------
39
+ A : float or str
40
+ Probability of event A, either as a decimal (0-1), percentage (0-100), or odds ("X:Y")
41
+ B : float or str
42
+ Probability of event B, either as a decimal (0-1), percentage (0-100), or odds ("X:Y")
43
+ event : str
44
+ Syntax for the event combination:
45
+ - "A&B" or "A∩B": AND/intersection P(A∩B) = P(A) * P(B) # both A and B occur, assuming independence
46
+ - "A/B" or "A∪B": OR/union P(A∪B) = P(A) + P(B) - P(A∩B) # either A or B or both occur
47
+ - "!A" or "¬A": NOT/complement P(¬A) = 1 - P(A) # A does not occur
48
+ - "A^B" or "A⊕B": XOR P(A⊕B) = P(A) + P(B) - 2*P(A∩B) # either A or B occurs, but not both
49
+ - "A-B" or "A∖B": Difference P(A∖B) = P(A) - P(A∩B) # A occurs but not B (instead of just: B not occuring)
50
+ - "none(A, B)": P(none) = ∏(1-P(i)) # neither A nor B occur
51
+ - "atleast1(A, B)": P(at least one) = 1 - P(none) # at least one of A or B occurs
52
+ output_scale : str
53
+ Output format:
54
+ "prob" (0-1), "percent" (0-100%), or "odds" (X:Y)
55
+
56
+ Returns:
57
+ --------
58
+ float or str
59
+ Combined probability in requested scale
60
+ """
61
+
62
+ # Convert inputs to probability scale (0-1) by auto-detecting format:
63
+ # "X:Y" string -> odds, value > 1 -> percent (0-100), otherwise -> prob (0-1)
64
+ def to_prob(val):
65
+ if isinstance(val, str) and ":" in val:
66
+ x, y = map(float, val.split(':'))
67
+ return x / (x + y)
68
+ if isinstance(val, str) and "%" in val:
69
+ return float(val.strip('%')) / 100
70
+ elif val > 1:
71
+ return val / 100
72
+ return val
73
+
74
+ def from_prob(val, out_scale=output_scale):
75
+ val = _round_sig(val) # remove floating point noise without destroying very small values
76
+ if out_scale == "percent" or out_scale == "%" or out_scale == "perc" or out_scale == "percentage":
77
+ return _to_decimal_str(_round_sig(val * 100)) + "%"
78
+ elif out_scale == "odds":
79
+ if val == 0:
80
+ return "0:1"
81
+ elif val == 1:
82
+ return "1:0"
83
+ if val < 0.001:
84
+ # Very small probability: Fraction.limit_denominator(1000) would round to 0, so compute directly
85
+ denom = int(_round_sig(1 / val, sig=3))
86
+ return f"1:{denom}"
87
+ # Convert to simplified integer odds A:B (e.g. 0.25 -> "1:3") expressing the ratio of favorable outcomes to unfavorable outcomes
88
+ frac = Fraction(val).limit_denominator(1000)
89
+ a, b = frac.numerator, frac.denominator - frac.numerator
90
+ return f"{a}:{b}"
91
+ return val
92
+
93
+ p_a = to_prob(A)
94
+ p_b = to_prob(B)
95
+
96
+ event = event.strip()
97
+
98
+ # AND / Intersection
99
+ if "&" in event or "∩" in event:
100
+ result = p_a * p_b
101
+
102
+ # OR / Union
103
+ elif "/" in event or "∪" in event:
104
+ result = p_a + p_b - (p_a * p_b)
105
+
106
+ # NOT / Complement
107
+ elif event.startswith("!") or event.startswith("¬"):
108
+ result = 1 - p_a
109
+
110
+ # XOR
111
+ elif "^" in event or "⊕" in event:
112
+ result = p_a + p_b - 2 * (p_a * p_b)
113
+
114
+ # Difference
115
+ elif "-" in event or "∖" in event:
116
+ result = p_a - (p_a * p_b)
117
+
118
+ # None (all fail)
119
+ elif event.startswith("none"):
120
+ # Extract all probability arguments
121
+ probs = [p_a, p_b]
122
+ result = 1
123
+ for p in probs:
124
+ result *= (1 - p)
125
+
126
+ # At least one
127
+ elif event.startswith("atleast1"):
128
+ probs = [p_a, p_b]
129
+ p_none = 1
130
+ for p in probs:
131
+ p_none *= (1 - p)
132
+ result = 1 - p_none
133
+
134
+ else:
135
+ raise ValueError(f"Unknown event syntax: {event}")
136
+
137
+ return from_prob(result)
138
+
139
+ #To do:
140
+ # - Improve code to handle very low probabilities
141
+ # - Add support for more than 2 events (e.g. A, B, C, ...)
142
+ # - Add a function for Bayesian updating P(A|B) = P(B|A)*P(A)/P(B)
143
+ # - Multi-event combinations like: exactly k of n events occur (independent Bernoulli), or at least k of n occur
@@ -0,0 +1,63 @@
1
+ """
2
+ Docstring for the series module
3
+
4
+ This module provides functionality to calculate series probabilities and outcomes.
5
+
6
+ """
7
+ import math
8
+
9
+ def at_least_k_of_n(k, n, p, output='result'):
10
+ """Calculate the probability of getting at least k successes in n independent Bernoulli trials with success probability p.
11
+
12
+ Parameters:
13
+ -----------
14
+ k : int
15
+ Minimum number of successes
16
+ n : int
17
+ Total number of trials
18
+ p : float
19
+ Probability of success on each trial (0-1). Input can also be a fraction (e.g. 1/3)
20
+ output : str, optional
21
+ Output format: 'result' (default, numeric) or 'verbose' (string)
22
+
23
+ """
24
+ if k > n:
25
+ return 0.0
26
+ if k <= 0:
27
+ return 1.0
28
+ # Calculate the cumulative probability of getting fewer than k successes
29
+ cumulative_prob = sum(math.comb(n, i) * (p ** i) * ((1 - p) ** (n - i)) for i in range(k))
30
+ if output == 'verbose':
31
+ print(f"The probability of getting {k} or more successes after {n} attempts/trials (each having a {p*100:.2f}% probability of success) is:")
32
+ return f"{(1 - cumulative_prob)*100:.2f}%"
33
+ else:
34
+ return 1 - cumulative_prob
35
+
36
+
37
+ def gambler_ruin(i, n, p=0.5):
38
+ """Calculate the probability of ending up with n after starting at i, derived from the
39
+ gambler's ruin Markov chain, also used for the random walk problem.
40
+
41
+ Parameters:
42
+ -----------
43
+ i : int
44
+ Starting amount
45
+ n : int
46
+ Target amount
47
+ p : float, optional
48
+ Probability of winning each bet or getting closer to the target (default is 0.5)
49
+ Returns:
50
+ --------
51
+ float
52
+ Probability of reaching n without first going broke (reaching 0)
53
+ """
54
+ if i <= 0:
55
+ return 0.0
56
+ if i >= n:
57
+ return 1.0
58
+ if p == 0.5:
59
+ return i / n
60
+ else:
61
+ q = 1 - p
62
+ return (1 - (q / p) ** i) / (1 - (q / p) ** n)
63
+ #Validated using https://zcalculators.com/calculator/gamblers-ruin
@@ -0,0 +1,7 @@
1
+ """Dice rolling tools for ProbabilityPewter."""
2
+
3
+ from .dice import rpg_dice
4
+ from .compare import prob_table
5
+ from .combat import combat_odds, risk_odds, ti_odds, aa_odds
6
+
7
+ __all__ = ["rpg_dice", "prob_table", "combat_odds"]