Decision rules
All decision rules take the same input — a dict of {choice: {scenario: payoff}} —
and return a solver with a .solve() method. They differ only in the attitude towards
uncertainty they encode. Payoffs can be integers, or floats.
outcomes = {
"stocks": {"recession": -20, "stagnation": 5, "growth": 30},
"bonds": {"recession": 5, "stagnation": 5, "growth": 7},
"cash": {"recession": 2, "stagnation": 2, "growth": 2},
}
Maximin
Which choice has the best worst case?
Pessimistic. Assumes the worst scenario will occur and picks the choice that hurts least. Useful when downside protection matters more than upside.
from formative.game import maximin
print(maximin(outcomes).solve())
MaximinResult(
stocks worst case: -20
bonds worst case: +5 ← chosen
cash worst case: +2
)
Bonds win: their worst case (recession, +5) is better than stocks (−20) or cash (+2).
- class formative.game.Maximin(outcomes)
Maximin decision rule.
- Parameters:
outcomes (dict of dict) – Mapping of
{choice: {scenario: payoff}}.
Examples
>>> result = maximin({ ... "stocks": {"recession": -20, "stagnation": 5, "growth": 30}, ... "bonds": {"recession": 5, "stagnation": 5, "growth": 7}, ... "cash": {"recession": 2, "stagnation": 2, "growth": 2}, ... }).solve() >>> result.choice 'bonds'
- solve()
Apply the maximin rule and return the safest choice.
- Returns:
MaximinResult
- class formative.game.MaximinResult(choice, guaranteed, worst_cases)
Result of applying the maximin decision rule.
- choice
The choice that maximises the worst-case payoff.
- Type:
str
- guaranteed
Worst-case payoff of the chosen action.
- Type:
float
- worst_cases
Worst-case payoff for every choice: {choice: float}.
- Type:
dict
- Parameters:
choice (
str)guaranteed (
float)worst_cases (
dict)
Maximax
Which choice has the best best case?
Optimistic. Assumes the best scenario will occur and picks accordingly. Appropriate when you can absorb losses and want to maximise upside.
from formative.game import maximax
print(maximax(outcomes).solve())
MaximaxResult(
stocks best case: +30 ← chosen
bonds best case: +7
cash best case: +2
)
Stocks win: their best case (growth, +30) is highest, even though they have the worst downside.
- class formative.game.Maximax(outcomes)
Maximax decision rule.
- Parameters:
outcomes (dict of dict) – Mapping of
{choice: {scenario: payoff}}.
Examples
>>> result = maximax({ ... "stocks": {"recession": -20, "stagnation": 5, "growth": 30}, ... "bonds": {"recession": 5, "stagnation": 5, "growth": 7}, ... "cash": {"recession": 2, "stagnation": 2, "growth": 2}, ... }).solve() >>> result.choice 'stocks'
- solve()
Apply the maximax rule and return the most optimistic choice.
- Returns:
MaximaxResult
- class formative.game.MaximaxResult(choice, best_case, best_cases)
Result of applying the maximax decision rule.
- choice
The choice with the best best-case payoff.
- Type:
str
- best_case
The best possible payoff for the chosen choice.
- Type:
float
- best_cases
Best-case payoff for every choice: {choice: float}.
- Type:
dict
- Parameters:
choice (
str)best_case (
float)best_cases (
dict)
Minimax Regret
Which choice do you regret the least in hindsight?
Regret in a given scenario is the gap between what you received and the best you could have received in that scenario. Minimax regret picks the choice where the worst-case regret is smallest — a middle ground between maximin and maximax.
First, find the best available payoff in each scenario: recession = 5 (bonds), stagnation = 5 (stocks or bonds), growth = 30 (stocks).
Then compute regret for every combination — how much you miss out on versus the best choice:
recession |
stagnation |
growth |
max regret |
|
|---|---|---|---|---|
stocks |
25 |
0 |
0 |
25 |
bonds |
0 |
0 |
23 |
23 ← lowest |
cash |
3 |
3 |
28 |
28 |
from formative.game import minimax_regret
result = minimax_regret(outcomes).solve()
print(result)
MinimaxRegretResult(
stocks max regret: +25
bonds max regret: +23 ← chosen
cash max regret: +28
)
Stocks score zero regret in growth (they are the best there) but 25 in recession. Bonds score zero in recession and stagnation, but 23 in growth where stocks outperform them. Cash never comes close to the best option in any scenario, giving it the highest max regret of 28. Bonds minimise the worst-case regret.
- class formative.game.MinimaxRegret(outcomes)
Minimax regret decision rule.
- Parameters:
outcomes (dict of dict) – Mapping of
{choice: {scenario: payoff}}.
Examples
>>> result = minimax_regret({ ... "stocks": {"recession": -20, "stagnation": 5, "growth": 30}, ... "bonds": {"recession": 5, "stagnation": 5, "growth": 7}, ... "cash": {"recession": 2, "stagnation": 2, "growth": 2}, ... }).solve() >>> result.choice 'bonds'
- solve()
Apply the minimax regret rule.
- Returns:
MinimaxRegretResult
- class formative.game.MinimaxRegretResult(choice, max_regret, max_regrets, regret_table)
Result of applying the minimax regret decision rule.
Regret for a given choice and scenario is the difference between the best payoff available in that scenario and the payoff actually received. The minimax regret rule picks the choice whose worst-case regret is smallest.
- choice
The choice that minimises the maximum regret.
- Type:
str
- max_regret
The maximum regret for the chosen choice.
- Type:
float
- max_regrets
Maximum regret for every choice: {choice: float}.
- Type:
dict
- regret_table
Full regret for every (choice, scenario) pair.
- Type:
dict of dict
- Parameters:
choice (
str)max_regret (
float)max_regrets (
dict)regret_table (
dict)
Hurwicz Criterion
Which choice scores best on a blend of optimism and pessimism?
The Hurwicz criterion scores each choice as:
The parameter alpha controls how optimistic you are. At alpha=0 the rule is
identical to maximin (pure pessimism); at alpha=1 it is identical to maximax (pure
optimism). Values in between express a considered attitude towards risk.
With alpha=0.5 (equal weight to best and worst case):
stocks: 0.5 × 30 + 0.5 × (−20) = +5
bonds: 0.5 × 7 + 0.5 × 5 = +6 ← chosen
cash: 0.5 × 2 + 0.5 × 2 = +2
from formative.game import hurwicz
print(hurwicz(outcomes, alpha=0.5).solve())
HurwiczResult(alpha=0.5,
stocks score: +5
bonds score: +6 ← chosen
cash score: +2
)
At alpha=0.8 the optimistic weight dominates and stocks pull ahead:
print(hurwicz(outcomes, alpha=0.8).solve())
HurwiczResult(alpha=0.8,
stocks score: +20 ← chosen
bonds score: +6.6
cash score: +2
)
- class formative.game.Hurwicz(outcomes, alpha)
Hurwicz decision rule.
- Parameters:
outcomes (dict of dict) – Mapping of
{choice: {scenario: payoff}}.alpha (float) – Optimism coefficient in [0, 1]. At alpha=0 the rule reduces to maximin (pessimistic); at alpha=1 it reduces to maximax (optimistic).
Examples
>>> result = hurwicz({ ... "stocks": {"recession": -20, "stagnation": 5, "growth": 30}, ... "bonds": {"recession": 5, "stagnation": 5, "growth": 7}, ... "cash": {"recession": 2, "stagnation": 2, "growth": 2}, ... }, alpha=0.5).solve() >>> result.choice 'bonds'
- solve()
Apply the Hurwicz rule and return the chosen action.
- Returns:
HurwiczResult
- class formative.game.HurwiczResult(choice, score, alpha, scores)
Result of applying the Hurwicz decision rule.
- choice
The choice with the highest Hurwicz score.
- Type:
str
- score
Hurwicz score of the chosen choice.
- Type:
float
- alpha
The optimism coefficient used (0 = pure maximin, 1 = pure maximax).
- Type:
float
- scores
Hurwicz score for every choice: {choice: float}.
- Type:
dict
- Parameters:
choice (
str)score (
float)alpha (
float)scores (
dict)
Laplace Criterion
Which choice has the best average payoff?
With no information about which scenario is more likely, treat them all as equally probable (Laplace’s principle of indifference) and pick the choice with the highest average payoff.
stocks: (−20 + 5 + 30) ÷ 3 = +5.0
bonds: (5 + 5 + 7) ÷ 3 = +5.67 ← chosen
cash: (2 + 2 + 2) ÷ 3 = +2.0
from formative.game import laplace
print(laplace(outcomes).solve())
LaplaceResult(
stocks average: +5
bonds average: +5.667 ← chosen
cash average: +2
)
- class formative.game.Laplace(outcomes)
Laplace decision rule.
- Parameters:
outcomes (dict of dict) – Mapping of
{choice: {scenario: payoff}}.
Examples
>>> result = laplace({ ... "stocks": {"recession": -20, "stagnation": 5, "growth": 30}, ... "bonds": {"recession": 5, "stagnation": 5, "growth": 7}, ... "cash": {"recession": 2, "stagnation": 2, "growth": 2}, ... }).solve() >>> result.choice 'stocks'
- solve()
Apply the Laplace rule and return the choice with the highest average payoff.
- Returns:
LaplaceResult
- class formative.game.LaplaceResult(choice, average, averages)
Result of applying the Laplace decision rule.
- choice
The choice with the highest average payoff.
- Type:
str
- average
Average payoff of the chosen choice.
- Type:
float
- averages
Average payoff for every choice: {choice: float}.
- Type:
dict
- Parameters:
choice (
str)average (
float)averages (
dict)
Expected Value
Which choice has the highest expected payoff given your probability beliefs?
When you have beliefs about how likely each scenario is, use those directly rather than assuming equal probability. The expected payoff of a choice is the probability-weighted sum of its payoffs across all scenarios.
With probabilities recession=0.2, stagnation=0.5, growth=0.3:
stocks: 0.2 × (−20) + 0.5 × 5 + 0.3 × 30 = +7.5 ← chosen
bonds: 0.2 × 5 + 0.5 × 5 + 0.3 × 7 = +5.6
cash: 0.2 × 2 + 0.5 × 2 + 0.3 × 2 = +2.0
from formative.game import expected_value
print(expected_value(outcomes, probabilities={
"recession": 0.2,
"stagnation": 0.5,
"growth": 0.3,
}).solve())
ExpectedValueResult(
stocks E[payoff]: +7.5 ← chosen
bonds E[payoff]: +5.6
cash E[payoff]: +2
)
Unlike the Laplace criterion, which treats all scenarios as equally likely, expected value lets you express a considered view. If you are uncertain about your probabilities, compare the result against Laplace or Hurwicz to see how sensitive the choice is to that assumption.
- class formative.game.ExpectedValue(outcomes, probabilities)
Expected value decision rule with explicit scenario probabilities.
- Parameters:
outcomes (dict of dict) – Mapping of
{choice: {scenario: payoff}}.probabilities (dict) – Mapping of
{scenario: probability}. Must cover every scenario present in outcomes and sum to 1.0 (within 1e-6 tolerance).
Examples
>>> result = expected_value({ ... "stocks": {"recession": -20, "stagnation": 5, "growth": 30}, ... "bonds": {"recession": 5, "stagnation": 5, "growth": 7}, ... "cash": {"recession": 2, "stagnation": 2, "growth": 2}, ... }, probabilities={"recession": 0.2, "stagnation": 0.5, "growth": 0.3}).solve() >>> result.choice 'stocks'
- solve()
Apply the expected value rule and return the choice with the highest expected payoff.
- Returns:
ExpectedValueResult
- class formative.game.ExpectedValueResult(choice, expected, expected_values, probabilities)
Result of applying the expected value decision rule.
- choice
The choice with the highest expected payoff.
- Type:
str
- expected
Expected payoff of the chosen choice.
- Type:
float
- expected_values
Expected payoff for every choice: {choice: float}.
- Type:
dict
- probabilities
Scenario probabilities used: {scenario: float}.
- Type:
dict
- Parameters:
choice (
str)expected (
float)expected_values (
dict)probabilities (
dict)